Store mapping from hashes for Ethereum transactions to Filecoin Message Cids

This commit is contained in:
Geoff Stuart 2023-01-04 08:22:41 -05:00
parent b6eb7fcd96
commit a8436074a6
28 changed files with 943 additions and 74 deletions

View File

@ -740,6 +740,11 @@ workflows:
suite: itest-eth_filter suite: itest-eth_filter
target: "./itests/eth_filter_test.go" target: "./itests/eth_filter_test.go"
- test:
name: test-itest-eth_hash_lookup
suite: itest-eth_hash_lookup
target: "./itests/eth_hash_lookup_test.go"
- test: - test:
name: test-itest-eth_transactions name: test-itest-eth_transactions
suite: itest-eth_transactions suite: itest-eth_transactions

View File

@ -778,6 +778,7 @@ type FullNode interface {
EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) //perm:read EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) //perm:read
EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) //perm:read EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) //perm:read
EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) //perm:read EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) //perm:read
EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) //perm:read
EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkOpt string) (ethtypes.EthUint64, error) //perm:read EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkOpt string) (ethtypes.EthUint64, error) //perm:read
EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*EthTxReceipt, error) //perm:read EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*EthTxReceipt, error) //perm:read
EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (ethtypes.EthTx, error) //perm:read EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (ethtypes.EthTx, error) //perm:read

View File

@ -1252,6 +1252,21 @@ func (mr *MockFullNodeMockRecorder) EthGetTransactionCount(arg0, arg1, arg2 inte
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetTransactionCount", reflect.TypeOf((*MockFullNode)(nil).EthGetTransactionCount), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetTransactionCount", reflect.TypeOf((*MockFullNode)(nil).EthGetTransactionCount), arg0, arg1, arg2)
} }
// EthGetTransactionHashByCid mocks base method.
func (m *MockFullNode) EthGetTransactionHashByCid(arg0 context.Context, arg1 cid.Cid) (*ethtypes.EthHash, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EthGetTransactionHashByCid", arg0, arg1)
ret0, _ := ret[0].(*ethtypes.EthHash)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EthGetTransactionHashByCid indicates an expected call of EthGetTransactionHashByCid.
func (mr *MockFullNodeMockRecorder) EthGetTransactionHashByCid(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetTransactionHashByCid", reflect.TypeOf((*MockFullNode)(nil).EthGetTransactionHashByCid), arg0, arg1)
}
// EthGetTransactionReceipt mocks base method. // EthGetTransactionReceipt mocks base method.
func (m *MockFullNode) EthGetTransactionReceipt(arg0 context.Context, arg1 ethtypes.EthHash) (*api.EthTxReceipt, error) { func (m *MockFullNode) EthGetTransactionReceipt(arg0 context.Context, arg1 ethtypes.EthHash) (*api.EthTxReceipt, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -263,6 +263,8 @@ type FullNodeStruct struct {
EthGetTransactionCount func(p0 context.Context, p1 ethtypes.EthAddress, p2 string) (ethtypes.EthUint64, error) `perm:"read"` EthGetTransactionCount func(p0 context.Context, p1 ethtypes.EthAddress, p2 string) (ethtypes.EthUint64, error) `perm:"read"`
EthGetTransactionHashByCid func(p0 context.Context, p1 cid.Cid) (*ethtypes.EthHash, error) `perm:"read"`
EthGetTransactionReceipt func(p0 context.Context, p1 ethtypes.EthHash) (*EthTxReceipt, error) `perm:"read"` EthGetTransactionReceipt func(p0 context.Context, p1 ethtypes.EthHash) (*EthTxReceipt, error) `perm:"read"`
EthMaxPriorityFeePerGas func(p0 context.Context) (ethtypes.EthBigInt, error) `perm:"read"` EthMaxPriorityFeePerGas func(p0 context.Context) (ethtypes.EthBigInt, error) `perm:"read"`
@ -2172,6 +2174,17 @@ func (s *FullNodeStub) EthGetTransactionCount(p0 context.Context, p1 ethtypes.Et
return *new(ethtypes.EthUint64), ErrNotSupported return *new(ethtypes.EthUint64), ErrNotSupported
} }
func (s *FullNodeStruct) EthGetTransactionHashByCid(p0 context.Context, p1 cid.Cid) (*ethtypes.EthHash, error) {
if s.Internal.EthGetTransactionHashByCid == nil {
return nil, ErrNotSupported
}
return s.Internal.EthGetTransactionHashByCid(p0, p1)
}
func (s *FullNodeStub) EthGetTransactionHashByCid(p0 context.Context, p1 cid.Cid) (*ethtypes.EthHash, error) {
return nil, ErrNotSupported
}
func (s *FullNodeStruct) EthGetTransactionReceipt(p0 context.Context, p1 ethtypes.EthHash) (*EthTxReceipt, error) { func (s *FullNodeStruct) EthGetTransactionReceipt(p0 context.Context, p1 ethtypes.EthHash) (*EthTxReceipt, error) {
if s.Internal.EthGetTransactionReceipt == nil { if s.Internal.EthGetTransactionReceipt == nil {
return nil, ErrNotSupported return nil, ErrNotSupported

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,162 @@
package chain
import (
"database/sql"
"math"
"github.com/ipfs/go-cid"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
)
var pragmas = []string{
"PRAGMA synchronous = normal",
"PRAGMA temp_store = memory",
"PRAGMA mmap_size = 30000000000",
"PRAGMA page_size = 32768",
"PRAGMA auto_vacuum = NONE",
"PRAGMA automatic_index = OFF",
"PRAGMA journal_mode = WAL",
"PRAGMA read_uncommitted = ON",
}
var ddls = []string{
`CREATE TABLE IF NOT EXISTS tx_hash_lookup (
hash TEXT PRIMARY KEY,
cid TEXT NOT NULL,
epoch INT NOT NULL
)`,
// metadata containing version of schema
`CREATE TABLE IF NOT EXISTS _meta (
version UINT64 NOT NULL UNIQUE
)`,
// version 1.
`INSERT OR IGNORE INTO _meta (version) VALUES (1)`,
}
const schemaVersion = 1
const MemPoolEpoch = math.MaxInt64
const (
insertTxHash = `INSERT INTO tx_hash_lookup
(hash, cid, epoch)
VALUES(?, ?, ?)
ON CONFLICT (hash) DO UPDATE SET epoch = EXCLUDED.epoch
WHERE epoch > EXCLUDED.epoch`
)
type TransactionHashLookup struct {
db *sql.DB
}
func (ei *TransactionHashLookup) InsertTxHash(txHash ethtypes.EthHash, c cid.Cid, epoch int64) error {
hashEntry, err := ei.db.Prepare(insertTxHash)
if err != nil {
return xerrors.Errorf("prepare insert event: %w", err)
}
_, err = hashEntry.Exec(txHash.String(), c.String(), epoch)
return err
}
func (ei *TransactionHashLookup) LookupCidFromTxHash(txHash ethtypes.EthHash) (cid.Cid, error) {
q, err := ei.db.Query("SELECT cid FROM tx_hash_lookup WHERE hash = :hash;", sql.Named("hash", txHash.String()))
if err != nil {
return cid.Undef, err
}
var c string
if !q.Next() {
return cid.Undef, xerrors.Errorf("transaction hash %s not found", txHash.String())
}
err = q.Scan(&c)
if err != nil {
return cid.Undef, err
}
return cid.Decode(c)
}
func (ei *TransactionHashLookup) LookupTxHashFromCid(c cid.Cid) (ethtypes.EthHash, error) {
q, err := ei.db.Query("SELECT hash FROM tx_hash_lookup WHERE cid = :cid;", sql.Named("cid", c.String()))
if err != nil {
return ethtypes.EmptyEthHash, err
}
var hashString string
if !q.Next() {
return ethtypes.EmptyEthHash, xerrors.Errorf("transaction hash %s not found", c.String())
}
err = q.Scan(&hashString)
if err != nil {
return ethtypes.EmptyEthHash, err
}
return ethtypes.ParseEthHash(hashString)
}
func (ei *TransactionHashLookup) RemoveEntriesOlderThan(epoch abi.ChainEpoch) (int64, error) {
res, err := ei.db.Exec("DELETE FROM tx_hash_lookup WHERE epoch < :epoch;", sql.Named("epoch", epoch))
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func NewTransactionHashLookup(path string) (*TransactionHashLookup, error) {
db, err := sql.Open("sqlite3", path+"?mode=rwc")
if err != nil {
return nil, xerrors.Errorf("open sqlite3 database: %w", err)
}
for _, pragma := range pragmas {
if _, err := db.Exec(pragma); err != nil {
_ = db.Close()
return nil, xerrors.Errorf("exec pragma %q: %w", pragma, err)
}
}
q, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta';")
if err == sql.ErrNoRows || !q.Next() {
// empty database, create the schema
for _, ddl := range ddls {
if _, err := db.Exec(ddl); err != nil {
_ = db.Close()
return nil, xerrors.Errorf("exec ddl %q: %w", ddl, err)
}
}
} else if err != nil {
_ = db.Close()
return nil, xerrors.Errorf("looking for _meta table: %w", err)
} else {
// Ensure we don't open a database from a different schema version
row := db.QueryRow("SELECT max(version) FROM _meta")
var version int
err := row.Scan(&version)
if err != nil {
_ = db.Close()
return nil, xerrors.Errorf("invalid database version: no version found")
}
if version != schemaVersion {
_ = db.Close()
return nil, xerrors.Errorf("invalid database version: got %d, expected %d", version, schemaVersion)
}
}
return &TransactionHashLookup{
db: db,
}, nil
}
func (ei *TransactionHashLookup) Close() error {
if ei.db == nil {
return nil
}
return ei.db.Close()
}

View File

@ -5,7 +5,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/ipfs/go-cid"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
@ -18,7 +17,7 @@ type MemPoolFilter struct {
ch chan<- interface{} ch chan<- interface{}
mu sync.Mutex mu sync.Mutex
collected []cid.Cid collected []*types.SignedMessage
lastTaken time.Time lastTaken time.Time
} }
@ -55,10 +54,10 @@ func (f *MemPoolFilter) CollectMessage(ctx context.Context, msg *types.SignedMes
copy(f.collected, f.collected[1:]) copy(f.collected, f.collected[1:])
f.collected = f.collected[:len(f.collected)-1] f.collected = f.collected[:len(f.collected)-1]
} }
f.collected = append(f.collected, msg.Cid()) f.collected = append(f.collected, msg)
} }
func (f *MemPoolFilter) TakeCollectedMessages(context.Context) []cid.Cid { func (f *MemPoolFilter) TakeCollectedMessages(context.Context) []*types.SignedMessage {
f.mu.Lock() f.mu.Lock()
collected := f.collected collected := f.collected
f.collected = nil f.collected = nil

View File

@ -231,6 +231,36 @@ func (tx *EthTxArgs) ToRlpUnsignedMsg() ([]byte, error) {
return append([]byte{0x02}, encoded...), nil return append([]byte{0x02}, encoded...), nil
} }
func (tx *EthTx) ToEthTxArgs() EthTxArgs {
return EthTxArgs{
ChainID: int(tx.ChainID),
Nonce: int(tx.Nonce),
To: tx.To,
Value: big.Int(tx.Value),
MaxFeePerGas: big.Int(tx.MaxFeePerGas),
MaxPriorityFeePerGas: big.Int(tx.MaxPriorityFeePerGas),
GasLimit: int(tx.Gas),
Input: tx.Input,
V: big.Int(tx.V),
R: big.Int(tx.R),
S: big.Int(tx.S),
}
}
func (tx *EthTx) TxHash() (EthHash, error) {
ethTxArgs := tx.ToEthTxArgs()
return (&ethTxArgs).TxHash()
}
func (tx *EthTxArgs) TxHash() (EthHash, error) {
rlp, err := tx.ToRlpSignedMsg()
if err != nil {
return EmptyEthHash, err
}
return EthHashFromTxBytes(rlp), nil
}
func (tx *EthTxArgs) ToRlpSignedMsg() ([]byte, error) { func (tx *EthTxArgs) ToRlpSignedMsg() ([]byte, error) {
packed1, err := tx.packTxFields() packed1, err := tx.packTxFields()
if err != nil { if err != nil {

View File

@ -392,10 +392,21 @@ func ParseEthHash(s string) (EthHash, error) {
return h, nil return h, nil
} }
func EthHashFromTxBytes(b []byte) EthHash {
hasher := sha3.NewLegacyKeccak256()
hasher.Write(b)
hash := hasher.Sum(nil)
var ethHash EthHash
copy(ethHash[:], hash)
return ethHash
}
func (h EthHash) String() string { func (h EthHash) String() string {
return "0x" + hex.EncodeToString(h[:]) return "0x" + hex.EncodeToString(h[:])
} }
// Should ONLY be used for blocks and Filecoin messages. Eth transactions expect a different hashing scheme.
func (h EthHash) ToCid() cid.Cid { func (h EthHash) ToCid() cid.Cid {
// err is always nil // err is always nil
mh, _ := multihash.EncodeName(h[:], "blake2b-256") mh, _ := multihash.EncodeName(h[:], "blake2b-256")
@ -556,7 +567,7 @@ type EthLog struct {
// The index corresponds to the sequence of messages produced by ChainGetParentMessages // The index corresponds to the sequence of messages produced by ChainGetParentMessages
TransactionIndex EthUint64 `json:"transactionIndex"` TransactionIndex EthUint64 `json:"transactionIndex"`
// TransactionHash is the cid of the message that produced the event log. // TransactionHash is the hash of the RLP message that produced the event log.
TransactionHash EthHash `json:"transactionHash"` TransactionHash EthHash `json:"transactionHash"`
// BlockHash is the hash of the tipset containing the message that produced the log. // BlockHash is the hash of the tipset containing the message that produced the log.

View File

@ -88,6 +88,7 @@
* [EthGetTransactionByBlockNumberAndIndex](#EthGetTransactionByBlockNumberAndIndex) * [EthGetTransactionByBlockNumberAndIndex](#EthGetTransactionByBlockNumberAndIndex)
* [EthGetTransactionByHash](#EthGetTransactionByHash) * [EthGetTransactionByHash](#EthGetTransactionByHash)
* [EthGetTransactionCount](#EthGetTransactionCount) * [EthGetTransactionCount](#EthGetTransactionCount)
* [EthGetTransactionHashByCid](#EthGetTransactionHashByCid)
* [EthGetTransactionReceipt](#EthGetTransactionReceipt) * [EthGetTransactionReceipt](#EthGetTransactionReceipt)
* [EthMaxPriorityFeePerGas](#EthMaxPriorityFeePerGas) * [EthMaxPriorityFeePerGas](#EthMaxPriorityFeePerGas)
* [EthNewBlockFilter](#EthNewBlockFilter) * [EthNewBlockFilter](#EthNewBlockFilter)
@ -2778,6 +2779,22 @@ Inputs:
Response: `"0x5"` Response: `"0x5"`
### EthGetTransactionHashByCid
Perms: read
Inputs:
```json
[
{
"/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"
}
]
```
Response: `"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"`
### EthGetTransactionReceipt ### EthGetTransactionReceipt

View File

@ -343,3 +343,11 @@
#ActorEventDatabasePath = "" #ActorEventDatabasePath = ""
[EthTxHashConfig]
# EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids
#
# type: bool
# env var: LOTUS_ETHTXHASHCONFIG_ENABLEETHHASHTOFILECOINCIDMAPPING
#EnableEthHashToFilecoinCidMapping = true

2
extern/filecoin-ffi vendored

@ -1 +1 @@
Subproject commit 86eac2161f442945bffee3fbfe7d094c20b48dd3 Subproject commit c4adeb4532719acf7b1c182cb98a3cca7b955a14

View File

@ -41,6 +41,7 @@ func TestDeployment(t *testing.T) {
cfg.ActorEvent.EnableRealTimeFilterAPI = true cfg.ActorEvent.EnableRealTimeFilterAPI = true
return nil return nil
}), }),
kit.EthTxHashLookup(),
) )
ens.InterconnectAll().BeginMining(blockTime) ens.InterconnectAll().BeginMining(blockTime)

View File

@ -204,7 +204,7 @@ func TestEthNewFilterCatchAll(t *testing.T) {
kit.QuietMiningLogs() kit.QuietMiningLogs()
blockTime := 100 * time.Millisecond blockTime := 100 * time.Millisecond
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.RealTimeFilterAPI()) client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.RealTimeFilterAPI(), kit.EthTxHashLookup())
ens.InterconnectAll().BeginMining(blockTime) ens.InterconnectAll().BeginMining(blockTime)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
@ -289,9 +289,9 @@ func TestEthNewFilterCatchAll(t *testing.T) {
received := make(map[ethtypes.EthHash]msgInTipset) received := make(map[ethtypes.EthHash]msgInTipset)
for m := range msgChan { for m := range msgChan {
eh, err := ethtypes.EthHashFromCid(m.msg.Cid) eh, err := client.EthGetTransactionHashByCid(ctx, m.msg.Cid)
require.NoError(err) require.NoError(err)
received[eh] = m received[*eh] = m
} }
require.Equal(iterations, len(received), "all messages on chain") require.Equal(iterations, len(received), "all messages on chain")

View File

@ -0,0 +1,340 @@
package itests
import (
"context"
"encoding/hex"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
"github.com/filecoin-project/lotus/itests/kit"
"github.com/filecoin-project/lotus/node/config"
)
// TestTransactionHashLookup tests to see if lotus correctly stores a mapping from ethereum transaction hash to
// Filecoin Message Cid
func TestTransactionHashLookup(t *testing.T) {
kit.QuietMiningLogs()
blocktime := 1 * time.Second
client, _, ens := kit.EnsembleMinimal(
t,
kit.MockProofs(),
kit.ThroughRPC(),
kit.EthTxHashLookup(),
)
ens.InterconnectAll().BeginMining(blocktime)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// install contract
contractHex, err := os.ReadFile("./contracts/SimpleCoin.bin")
require.NoError(t, err)
contract, err := hex.DecodeString(string(contractHex))
require.NoError(t, err)
// create a new Ethereum account
key, ethAddr, deployer := client.EVM().NewAccount()
// send some funds to the f410 address
kit.SendFunds(ctx, t, client, deployer, types.FromFil(10))
gaslimit, err := client.EthEstimateGas(ctx, ethtypes.EthCall{
From: &ethAddr,
Data: contract,
})
require.NoError(t, err)
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)
// now deploy a contract from the embryo, and validate it went well
tx := ethtypes.EthTxArgs{
ChainID: build.Eip155ChainId,
Value: big.Zero(),
Nonce: 0,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
Input: contract,
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}
client.EVM().SignTransaction(&tx, key.PrivateKey)
rawTxHash, err := tx.TxHash()
require.NoError(t, err)
hash := client.EVM().SubmitTransaction(ctx, &tx)
require.Equal(t, rawTxHash, hash)
mpoolTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, mpoolTx.Hash)
// Wait for message to land on chain
var receipt *api.EthTxReceipt
for i := 0; i < 20; i++ {
receipt, err = client.EthGetTransactionReceipt(ctx, hash)
if err != nil || receipt == nil {
time.Sleep(blocktime)
continue
}
break
}
require.NoError(t, err)
require.NotNil(t, receipt)
// Verify that the chain transaction now has new fields set.
chainTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, chainTx.Hash)
// require that the hashes are identical
require.Equal(t, hash, chainTx.Hash)
require.NotNil(t, chainTx.BlockNumber)
require.Greater(t, uint64(*chainTx.BlockNumber), uint64(0))
require.NotNil(t, chainTx.BlockHash)
require.NotEmpty(t, *chainTx.BlockHash)
require.NotNil(t, chainTx.TransactionIndex)
require.Equal(t, uint64(*chainTx.TransactionIndex), uint64(0)) // only transaction
}
// TestTransactionHashLookupNoDb tests to see if looking up eth transactions by hash breaks without the lookup table
func TestTransactionHashLookupNoDb(t *testing.T) {
kit.QuietMiningLogs()
blocktime := 1 * time.Second
client, _, ens := kit.EnsembleMinimal(
t,
kit.MockProofs(),
kit.ThroughRPC(),
kit.WithCfgOpt(func(cfg *config.FullNode) error {
cfg.EthTxHashConfig.EnableEthHashToFilecoinCidMapping = false
return nil
}),
)
ens.InterconnectAll().BeginMining(blocktime)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// install contract
contractHex, err := os.ReadFile("./contracts/SimpleCoin.bin")
require.NoError(t, err)
contract, err := hex.DecodeString(string(contractHex))
require.NoError(t, err)
// create a new Ethereum account
key, ethAddr, deployer := client.EVM().NewAccount()
// send some funds to the f410 address
kit.SendFunds(ctx, t, client, deployer, types.FromFil(10))
gaslimit, err := client.EthEstimateGas(ctx, ethtypes.EthCall{
From: &ethAddr,
Data: contract,
})
require.NoError(t, err)
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)
// now deploy a contract from the embryo, and validate it went well
tx := ethtypes.EthTxArgs{
ChainID: build.Eip155ChainId,
Value: big.Zero(),
Nonce: 0,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
Input: contract,
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}
client.EVM().SignTransaction(&tx, key.PrivateKey)
rawTxHash, err := tx.TxHash()
require.NoError(t, err)
hash := client.EVM().SubmitTransaction(ctx, &tx)
require.Equal(t, rawTxHash, hash)
// We shouldn't be able to find the tx
mpoolTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Nil(t, mpoolTx)
// Wait for message to land on chain, we can't know exactly when because we can't find it.
time.Sleep(20 * blocktime)
receipt, err := client.EthGetTransactionReceipt(ctx, hash)
require.NoError(t, err)
require.Nil(t, receipt)
// We still shouldn't be able to find the tx
chainTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Nil(t, chainTx)
}
// TestTransactionHashLookupBlsFilecoinMessage tests to see if lotus can find a BLS Filecoin Message using the transaction hash
func TestTransactionHashLookupBlsFilecoinMessage(t *testing.T) {
kit.QuietMiningLogs()
blocktime := 1 * time.Second
client, _, ens := kit.EnsembleMinimal(
t,
kit.MockProofs(),
kit.ThroughRPC(),
kit.EthTxHashLookup(),
)
ens.InterconnectAll().BeginMining(blocktime)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// get the existing balance from the default wallet to then split it.
bal, err := client.WalletBalance(ctx, client.DefaultKey.Address)
require.NoError(t, err)
// create a new address where to send funds.
addr, err := client.WalletNew(ctx, types.KTBLS)
require.NoError(t, err)
toSend := big.Div(bal, big.NewInt(2))
msg := &types.Message{
From: client.DefaultKey.Address,
To: addr,
Value: toSend,
}
sm, err := client.MpoolPushMessage(ctx, msg, nil)
require.NoError(t, err)
hash, err := ethtypes.EthHashFromCid(sm.Message.Cid())
require.NoError(t, err)
mpoolTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, mpoolTx.Hash)
// Wait for message to land on chain
var receipt *api.EthTxReceipt
for i := 0; i < 20; i++ {
receipt, err = client.EthGetTransactionReceipt(ctx, hash)
if err != nil || receipt == nil {
time.Sleep(blocktime)
continue
}
break
}
require.NoError(t, err)
require.NotNil(t, receipt)
require.Equal(t, hash, receipt.TransactionHash)
// Verify that the chain transaction now has new fields set.
chainTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, chainTx.Hash)
// require that the hashes are identical
require.Equal(t, hash, chainTx.Hash)
require.NotNil(t, chainTx.BlockNumber)
require.Greater(t, uint64(*chainTx.BlockNumber), uint64(0))
require.NotNil(t, chainTx.BlockHash)
require.NotEmpty(t, *chainTx.BlockHash)
require.NotNil(t, chainTx.TransactionIndex)
require.Equal(t, uint64(*chainTx.TransactionIndex), uint64(0)) // only transaction
}
// TestTransactionHashLookupSecpFilecoinMessage tests to see if lotus can find a Secp Filecoin Message using the transaction hash
func TestTransactionHashLookupSecpFilecoinMessage(t *testing.T) {
kit.QuietMiningLogs()
blocktime := 1 * time.Second
client, _, ens := kit.EnsembleMinimal(
t,
kit.MockProofs(),
kit.ThroughRPC(),
kit.EthTxHashLookup(),
)
ens.InterconnectAll().BeginMining(blocktime)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// get the existing balance from the default wallet to then split it.
bal, err := client.WalletBalance(ctx, client.DefaultKey.Address)
require.NoError(t, err)
// create a new address where to send funds.
addr, err := client.WalletNew(ctx, types.KTSecp256k1)
require.NoError(t, err)
toSend := big.Div(bal, big.NewInt(2))
setupMsg := &types.Message{
From: client.DefaultKey.Address,
To: addr,
Value: toSend,
}
setupSmsg, err := client.MpoolPushMessage(ctx, setupMsg, nil)
require.NoError(t, err)
_, err = client.StateWaitMsg(ctx, setupSmsg.Cid(), 3, api.LookbackNoLimit, true)
require.NoError(t, err)
// Send message for secp account
secpMsg := &types.Message{
From: addr,
To: client.DefaultKey.Address,
Value: big.Div(toSend, big.NewInt(2)),
}
secpSmsg, err := client.MpoolPushMessage(ctx, secpMsg, nil)
require.NoError(t, err)
hash, err := ethtypes.EthHashFromCid(secpSmsg.Cid())
require.NoError(t, err)
mpoolTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, mpoolTx.Hash)
_, err = client.StateWaitMsg(ctx, secpSmsg.Cid(), 3, api.LookbackNoLimit, true)
require.NoError(t, err)
receipt, err := client.EthGetTransactionReceipt(ctx, hash)
require.NoError(t, err)
require.NotNil(t, receipt)
require.Equal(t, hash, receipt.TransactionHash)
// Verify that the chain transaction now has new fields set.
chainTx, err := client.EthGetTransactionByHash(ctx, &hash)
require.NoError(t, err)
require.Equal(t, hash, chainTx.Hash)
// require that the hashes are identical
require.Equal(t, hash, chainTx.Hash)
require.NotNil(t, chainTx.BlockNumber)
require.Greater(t, uint64(*chainTx.BlockNumber), uint64(0))
require.NotNil(t, chainTx.BlockHash)
require.NotEmpty(t, *chainTx.BlockHash)
require.NotNil(t, chainTx.TransactionIndex)
require.Equal(t, uint64(*chainTx.TransactionIndex), uint64(0)) // only transaction
}

View File

@ -296,3 +296,10 @@ func HistoricFilterAPI(dbpath string) NodeOpt {
return nil return nil
}) })
} }
func EthTxHashLookup() NodeOpt {
return WithCfgOpt(func(cfg *config.FullNode) error {
cfg.EthTxHashConfig.EnableEthHashToFilecoinCidMapping = true
return nil
})
}

View File

@ -161,7 +161,6 @@ var ChainNode = Options(
Override(new(messagepool.Provider), messagepool.NewProvider), Override(new(messagepool.Provider), messagepool.NewProvider),
Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))), Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))),
Override(new(full.ChainModuleAPI), From(new(full.ChainModule))), Override(new(full.ChainModuleAPI), From(new(full.ChainModule))),
Override(new(full.EthModuleAPI), From(new(full.EthModule))),
Override(new(full.GasModuleAPI), From(new(full.GasModule))), Override(new(full.GasModuleAPI), From(new(full.GasModule))),
Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))), Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))),
Override(new(full.StateModuleAPI), From(new(full.StateModule))), Override(new(full.StateModuleAPI), From(new(full.StateModule))),
@ -261,6 +260,8 @@ func ConfigFullNode(c interface{}) Option {
Override(new(events.EventAPI), From(new(modules.EventAPI))), Override(new(events.EventAPI), From(new(modules.EventAPI))),
// in lite-mode Eth event api is provided by gateway // in lite-mode Eth event api is provided by gateway
ApplyIf(isFullNode, Override(new(full.EthEventAPI), modules.EthEventAPI(cfg.ActorEvent))), ApplyIf(isFullNode, Override(new(full.EthEventAPI), modules.EthEventAPI(cfg.ActorEvent))),
Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.EthTxHashConfig)),
) )
} }

View File

@ -107,6 +107,9 @@ func DefaultFullNode() *FullNode {
MaxFilterResults: 10000, MaxFilterResults: 10000,
MaxFilterHeightRange: 2880, // conservative limit of one day MaxFilterHeightRange: 2880, // conservative limit of one day
}, },
EthTxHashConfig: EthTxHashConfig{
EnableEthHashToFilecoinCidMapping: true,
},
} }
} }

View File

@ -391,6 +391,14 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
Comment: ``, Comment: ``,
}, },
}, },
"EthTxHashConfig": []DocField{
{
Name: "EnableEthHashToFilecoinCidMapping",
Type: "bool",
Comment: `EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids`,
},
},
"FeeConfig": []DocField{ "FeeConfig": []DocField{
{ {
Name: "DefaultMaxFee", Name: "DefaultMaxFee",
@ -434,6 +442,12 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
Name: "ActorEvent", Name: "ActorEvent",
Type: "ActorEventConfig", Type: "ActorEventConfig",
Comment: ``,
},
{
Name: "EthTxHashConfig",
Type: "EthTxHashConfig",
Comment: ``, Comment: ``,
}, },
}, },

View File

@ -28,6 +28,7 @@ type FullNode struct {
Chainstore Chainstore Chainstore Chainstore
Cluster UserRaftConfig Cluster UserRaftConfig
ActorEvent ActorEventConfig ActorEvent ActorEventConfig
EthTxHashConfig EthTxHashConfig
} }
// // Common // // Common
@ -692,3 +693,8 @@ type ActorEventConfig struct {
// Set a timeout for subscription clients // Set a timeout for subscription clients
// Set upper bound on index size // Set upper bound on index size
} }
type EthTxHashConfig struct {
// EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids
EnableEthHashToFilecoinCidMapping bool
}

View File

@ -21,9 +21,11 @@ import (
builtintypes "github.com/filecoin-project/go-state-types/builtin" builtintypes "github.com/filecoin-project/go-state-types/builtin"
"github.com/filecoin-project/go-state-types/builtin/v10/eam" "github.com/filecoin-project/go-state-types/builtin/v10/eam"
"github.com/filecoin-project/go-state-types/builtin/v10/evm" "github.com/filecoin-project/go-state-types/builtin/v10/evm"
"github.com/filecoin-project/go-state-types/crypto"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain"
"github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/actors"
builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin"
"github.com/filecoin-project/lotus/chain/events/filter" "github.com/filecoin-project/lotus/chain/events/filter"
@ -43,6 +45,7 @@ type EthModuleAPI interface {
EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error)
EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error)
EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error)
EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error)
EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkOpt string) (ethtypes.EthUint64, error) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkOpt string) (ethtypes.EthUint64, error)
EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error)
EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (ethtypes.EthTx, error) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (ethtypes.EthTx, error)
@ -107,11 +110,10 @@ var (
// accepts as the best parent tipset, based on the blocks it is accumulating // accepts as the best parent tipset, based on the blocks it is accumulating
// within the HEAD tipset. // within the HEAD tipset.
type EthModule struct { type EthModule struct {
fx.In
Chain *store.ChainStore Chain *store.ChainStore
Mpool *messagepool.MessagePool Mpool *messagepool.MessagePool
StateManager *stmgr.StateManager StateManager *stmgr.StateManager
EthTxHashManager *EthTxHashManager
ChainAPI ChainAPI
MpoolAPI MpoolAPI
@ -254,10 +256,21 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype
return nil, nil return nil, nil
} }
cid := txHash.ToCid() c := cid.Undef
if a.EthTxHashManager != nil {
var err error
c, err = a.EthTxHashManager.TransactionHashLookup.LookupCidFromTxHash(*txHash)
if err != nil {
log.Debug("could not find transaction hash %s in lookup table", txHash.String())
}
}
// This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message
if c == cid.Undef {
c = txHash.ToCid()
}
// first, try to get the cid from mined transactions // first, try to get the cid from mined transactions
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, cid, api.LookbackNoLimit, true) msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, api.LookbackNoLimit, true)
if err == nil { if err == nil {
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI) tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI)
if err == nil { if err == nil {
@ -274,8 +287,8 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype
} }
for _, p := range pending { for _, p := range pending {
if p.Cid() == cid { if p.Cid() == c {
tx, err := newEthTxFromFilecoinMessage(ctx, p, a.StateAPI) tx, err := NewEthTxFromFilecoinMessage(ctx, p, a.StateAPI)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not convert Filecoin message into tx: %s", err) return nil, fmt.Errorf("could not convert Filecoin message into tx: %s", err)
} }
@ -286,6 +299,11 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype
return nil, nil return nil, nil
} }
func (a *EthModule) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) {
hash, err := EthTxHashFromFilecoinMessageCid(ctx, cid, a.StateAPI)
return &hash, err
}
func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam string) (ethtypes.EthUint64, error) { func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam string) (ethtypes.EthUint64, error) {
addr, err := sender.ToFilecoinAddress() addr, err := sender.ToFilecoinAddress()
if err != nil { if err != nil {
@ -305,19 +323,30 @@ func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes.
} }
func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) { func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) {
cid := txHash.ToCid() c := cid.Undef
if a.EthTxHashManager != nil {
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, cid, api.LookbackNoLimit, true) var err error
c, err = a.EthTxHashManager.TransactionHashLookup.LookupCidFromTxHash(txHash)
if err != nil { if err != nil {
log.Debug("could not find transaction hash %s in lookup table", txHash.String())
}
}
// This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message
if c == cid.Undef {
c = txHash.ToCid()
}
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, api.LookbackNoLimit, true)
if err != nil || msgLookup == nil {
return nil, nil return nil, nil
} }
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI) tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI)
if err != nil { if err != nil && tx.Hash == ethtypes.EmptyEthHash {
return nil, nil return nil, nil
} }
replay, err := a.StateAPI.StateReplay(ctx, types.EmptyTSK, cid) replay, err := a.StateAPI.StateReplay(ctx, types.EmptyTSK, c)
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
@ -640,11 +669,12 @@ func (a *EthModule) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.Et
smsg.Message.Method = builtinactors.MethodSend smsg.Message.Method = builtinactors.MethodSend
} }
cid, err := a.MpoolAPI.MpoolPush(ctx, smsg) _, err = a.MpoolAPI.MpoolPush(ctx, smsg)
if err != nil { if err != nil {
return ethtypes.EmptyEthHash, err return ethtypes.EmptyEthHash, err
} }
return ethtypes.EthHashFromCid(cid)
return ethtypes.EthHashFromTxBytes(rawTx), nil
} }
func (a *EthModule) ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) { func (a *EthModule) ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) {
@ -791,7 +821,7 @@ func (e *EthEvent) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilte
_ = e.uninstallFilter(ctx, f) _ = e.uninstallFilter(ctx, f)
return ethFilterResultFromEvents(ces) return ethFilterResultFromEvents(ces, e.SubManager.StateAPI)
} }
func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) {
@ -806,11 +836,11 @@ func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilte
switch fc := f.(type) { switch fc := f.(type) {
case filterEventCollector: case filterEventCollector:
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx)) return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI)
case filterTipSetCollector: case filterTipSetCollector:
return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx)) return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx))
case filterMessageCollector: case filterMessageCollector:
return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx)) return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx), e.SubManager.StateAPI)
} }
return nil, xerrors.Errorf("unknown filter type") return nil, xerrors.Errorf("unknown filter type")
@ -828,7 +858,7 @@ func (e *EthEvent) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID
switch fc := f.(type) { switch fc := f.(type) {
case filterEventCollector: case filterEventCollector:
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx)) return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI)
} }
return nil, xerrors.Errorf("wrong filter type") return nil, xerrors.Errorf("wrong filter type")
@ -1141,14 +1171,14 @@ type filterEventCollector interface {
} }
type filterMessageCollector interface { type filterMessageCollector interface {
TakeCollectedMessages(context.Context) []cid.Cid TakeCollectedMessages(context.Context) []*types.SignedMessage
} }
type filterTipSetCollector interface { type filterTipSetCollector interface {
TakeCollectedTipSets(context.Context) []types.TipSetKey TakeCollectedTipSets(context.Context) []types.TipSetKey
} }
func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*ethtypes.EthFilterResult, error) { func ethFilterResultFromEvents(evs []*filter.CollectedEvent, sa StateAPI) (*ethtypes.EthFilterResult, error) {
res := &ethtypes.EthFilterResult{} res := &ethtypes.EthFilterResult{}
for _, ev := range evs { for _, ev := range evs {
log := ethtypes.EthLog{ log := ethtypes.EthLog{
@ -1174,11 +1204,10 @@ func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*ethtypes.EthFilte
return nil, err return nil, err
} }
log.TransactionHash, err = ethtypes.EthHashFromCid(ev.MsgCid) log.TransactionHash, err = EthTxHashFromFilecoinMessageCid(context.TODO(), ev.MsgCid, sa)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err := ev.TipSetKey.Cid() c, err := ev.TipSetKey.Cid()
if err != nil { if err != nil {
return nil, err return nil, err
@ -1213,11 +1242,11 @@ func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*ethtypes.EthFilterResu
return res, nil return res, nil
} }
func ethFilterResultFromMessages(cs []cid.Cid) (*ethtypes.EthFilterResult, error) { func ethFilterResultFromMessages(cs []*types.SignedMessage, sa StateAPI) (*ethtypes.EthFilterResult, error) {
res := &ethtypes.EthFilterResult{} res := &ethtypes.EthFilterResult{}
for _, c := range cs { for _, c := range cs {
hash, err := ethtypes.EthHashFromCid(c) hash, err := EthTxHashFromSignedFilecoinMessage(context.TODO(), c, sa)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1316,7 +1345,7 @@ func (e *ethSubscription) start(ctx context.Context) {
var err error var err error
switch vt := v.(type) { switch vt := v.(type) {
case *filter.CollectedEvent: case *filter.CollectedEvent:
resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt}) resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt}, e.StateAPI)
case *types.TipSet: case *types.TipSet:
eb, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.ChainAPI, e.StateAPI) eb, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.ChainAPI, e.StateAPI)
if err != nil { if err != nil {
@ -1391,18 +1420,15 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx
} }
gasUsed += msgLookup.Receipt.GasUsed gasUsed += msgLookup.Receipt.GasUsed
if fullTxInfo {
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, txIdx, cs, sa) tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, txIdx, cs, sa)
if err != nil { if err != nil {
return ethtypes.EthBlock{}, nil return ethtypes.EthBlock{}, nil
} }
if fullTxInfo {
block.Transactions = append(block.Transactions, tx) block.Transactions = append(block.Transactions, tx)
} else { } else {
hash, err := ethtypes.EthHashFromCid(msg.Cid()) block.Transactions = append(block.Transactions, tx.Hash.String())
if err != nil {
return ethtypes.EthBlock{}, err
}
block.Transactions = append(block.Transactions, hash.String())
} }
} }
@ -1454,19 +1480,35 @@ func lookupEthAddress(ctx context.Context, addr address.Address, sa StateAPI) (e
return ethtypes.EthAddressFromFilecoinAddress(idAddr) return ethtypes.EthAddressFromFilecoinAddress(idAddr)
} }
func newEthTxFromFilecoinMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthTx, error) { func EthTxHashFromFilecoinMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) {
fromEthAddr, err := lookupEthAddress(ctx, smsg.Message.From, sa) smsg, err := sa.Chain.GetSignedMessage(ctx, c)
if err != nil { if err == nil {
return ethtypes.EthTx{}, err return EthTxHashFromSignedFilecoinMessage(ctx, smsg, sa)
} }
toEthAddr, err := lookupEthAddress(ctx, smsg.Message.To, sa) return ethtypes.EthHashFromCid(c)
}
func EthTxHashFromSignedFilecoinMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthHash, error) {
if smsg.Signature.Type == crypto.SigTypeDelegated {
ethTx, err := NewEthTxFromFilecoinMessage(ctx, smsg, sa)
if err != nil { if err != nil {
return ethtypes.EthTx{}, err return ethtypes.EmptyEthHash, err
} }
return ethTx.Hash, nil
}
return ethtypes.EthHashFromCid(smsg.Cid())
}
func NewEthTxFromFilecoinMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthTx, error) {
// Ignore errors here so we can still parse non-eth messages
fromEthAddr, _ := lookupEthAddress(ctx, smsg.Message.From, sa)
toEthAddr, _ := lookupEthAddress(ctx, smsg.Message.To, sa)
toAddr := &toEthAddr toAddr := &toEthAddr
input := smsg.Message.Params input := smsg.Message.Params
var err error
// Check to see if we need to decode as contract deployment. // Check to see if we need to decode as contract deployment.
// We don't need to resolve the to address, because there's only one form (an ID). // We don't need to resolve the to address, because there's only one form (an ID).
if smsg.Message.To == builtintypes.EthereumAddressManagerActorAddr { if smsg.Message.To == builtintypes.EthereumAddressManagerActorAddr {
@ -1505,26 +1547,38 @@ func newEthTxFromFilecoinMessage(ctx context.Context, smsg *types.SignedMessage,
r, s, v = ethtypes.EthBigIntZero, ethtypes.EthBigIntZero, ethtypes.EthBigIntZero r, s, v = ethtypes.EthBigIntZero, ethtypes.EthBigIntZero, ethtypes.EthBigIntZero
} }
hash, err := ethtypes.EthHashFromCid(smsg.Cid())
if err != nil {
return ethtypes.EthTx{}, err
}
tx := ethtypes.EthTx{ tx := ethtypes.EthTx{
Hash: hash,
Nonce: ethtypes.EthUint64(smsg.Message.Nonce), Nonce: ethtypes.EthUint64(smsg.Message.Nonce),
ChainID: ethtypes.EthUint64(build.Eip155ChainId), ChainID: ethtypes.EthUint64(build.Eip155ChainId),
From: fromEthAddr, From: fromEthAddr,
To: toAddr, To: toAddr,
Value: ethtypes.EthBigInt(smsg.Message.Value), Value: ethtypes.EthBigInt(smsg.Message.Value),
Type: ethtypes.EthUint64(2), Type: ethtypes.EthUint64(2),
Input: input,
Gas: ethtypes.EthUint64(smsg.Message.GasLimit), Gas: ethtypes.EthUint64(smsg.Message.GasLimit),
MaxFeePerGas: ethtypes.EthBigInt(smsg.Message.GasFeeCap), MaxFeePerGas: ethtypes.EthBigInt(smsg.Message.GasFeeCap),
MaxPriorityFeePerGas: ethtypes.EthBigInt(smsg.Message.GasPremium), MaxPriorityFeePerGas: ethtypes.EthBigInt(smsg.Message.GasPremium),
V: v, V: v,
R: r, R: r,
S: s, S: s,
Input: input, }
// This is an eth tx
if smsg.Signature.Type == crypto.SigTypeDelegated {
tx.Hash, err = tx.TxHash()
if err != nil {
return tx, err
}
} else if smsg.Signature.Type == crypto.SigTypeUnknown { // BLS Filecoin message
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid())
if err != nil {
return tx, err
}
} else { // Secp Filecoin Message
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid())
if err != nil {
return tx, err
}
} }
return tx, nil return tx, nil
@ -1537,11 +1591,6 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
if msgLookup == nil { if msgLookup == nil {
return ethtypes.EthTx{}, fmt.Errorf("msg does not exist") return ethtypes.EthTx{}, fmt.Errorf("msg does not exist")
} }
cid := msgLookup.Message
txHash, err := ethtypes.EthHashFromCid(cid)
if err != nil {
return ethtypes.EthTx{}, err
}
ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet) ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet)
if err != nil { if err != nil {
@ -1582,11 +1631,22 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
} }
smsg, err := cs.GetSignedMessage(ctx, msgLookup.Message) smsg, err := cs.GetSignedMessage(ctx, msgLookup.Message)
if err != nil {
// We couldn't find the signed message, it might be a BLS message, so search for a regular message.
msg, err := cs.GetMessage(ctx, msgLookup.Message)
if err != nil { if err != nil {
return ethtypes.EthTx{}, err return ethtypes.EthTx{}, err
} }
smsg = &types.SignedMessage{
Message: *msg,
Signature: crypto.Signature{
Type: crypto.SigTypeUnknown,
Data: nil,
},
}
}
tx, err := newEthTxFromFilecoinMessage(ctx, smsg, sa) tx, err := NewEthTxFromFilecoinMessage(ctx, smsg, sa)
if err != nil { if err != nil {
return ethtypes.EthTx{}, err return ethtypes.EthTx{}, err
} }
@ -1597,7 +1657,6 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
) )
tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId)
tx.Hash = txHash
tx.BlockHash = &blkHash tx.BlockHash = &blkHash
tx.BlockNumber = &bn tx.BlockNumber = &bn
tx.TransactionIndex = &ti tx.TransactionIndex = &ti
@ -1701,6 +1760,68 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLook
return receipt, nil return receipt, nil
} }
func (m EthTxHashManager) Apply(ctx context.Context, from, to *types.TipSet) error {
for _, blk := range to.Blocks() {
_, smsgs, err := m.StateAPI.Chain.MessagesForBlock(ctx, blk)
if err != nil {
return err
}
for _, smsg := range smsgs {
if smsg.Signature.Type != crypto.SigTypeDelegated {
continue
}
hash, err := EthTxHashFromSignedFilecoinMessage(ctx, smsg, m.StateAPI)
if err != nil {
return err
}
err = m.TransactionHashLookup.InsertTxHash(hash, smsg.Cid(), int64(to.Height()))
if err != nil {
return err
}
}
}
return nil
}
type EthTxHashManager struct {
StateAPI StateAPI
TransactionHashLookup *chain.TransactionHashLookup
}
func (m EthTxHashManager) Revert(ctx context.Context, from, to *types.TipSet) error {
return nil
}
func WaitForMpoolUpdates(ctx context.Context, ch <-chan api.MpoolUpdate, manager *EthTxHashManager) {
for {
select {
case <-ctx.Done():
return
case u := <-ch:
if u.Type != api.MpoolAdd {
continue
}
if u.Message.Signature.Type != crypto.SigTypeDelegated {
continue
}
ethTx, err := NewEthTxFromFilecoinMessage(ctx, u.Message, manager.StateAPI)
if err != nil {
log.Errorf("error converting filecoin message to eth tx: %s", err)
}
err = manager.TransactionHashLookup.InsertTxHash(ethTx.Hash, u.Message.Cid(), chain.MemPoolEpoch)
if err != nil {
log.Errorf("error inserting tx mapping to db: %s", err)
}
}
}
}
// decodeLogBytes decodes a CBOR-serialized array into its original form. // decodeLogBytes decodes a CBOR-serialized array into its original form.
// //
// This function swallows errors and returns the original array if it failed // This function swallows errors and returns the original array if it failed

84
node/modules/ethmodule.go Normal file
View File

@ -0,0 +1,84 @@
package modules
import (
"context"
"path/filepath"
"go.uber.org/fx"
"github.com/filecoin-project/lotus/chain"
"github.com/filecoin-project/lotus/chain/events"
"github.com/filecoin-project/lotus/chain/messagepool"
"github.com/filecoin-project/lotus/chain/stmgr"
"github.com/filecoin-project/lotus/chain/store"
"github.com/filecoin-project/lotus/node/config"
"github.com/filecoin-project/lotus/node/impl/full"
"github.com/filecoin-project/lotus/node/modules/helpers"
"github.com/filecoin-project/lotus/node/repo"
)
func EthModuleAPI(cfg config.EthTxHashConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *store.ChainStore, *stmgr.StateManager, EventAPI, *messagepool.MessagePool, full.StateAPI, full.ChainAPI, full.MpoolAPI) (*full.EthModule, error) {
return func(mctx helpers.MetricsCtx, r repo.LockedRepo, lc fx.Lifecycle, cs *store.ChainStore, sm *stmgr.StateManager, evapi EventAPI, mp *messagepool.MessagePool, stateapi full.StateAPI, chainapi full.ChainAPI, mpoolapi full.MpoolAPI) (*full.EthModule, error) {
em := &full.EthModule{
Chain: cs,
Mpool: mp,
StateManager: sm,
ChainAPI: chainapi,
MpoolAPI: mpoolapi,
StateAPI: stateapi,
}
if !cfg.EnableEthHashToFilecoinCidMapping {
// mapping functionality disabled. Nothing to do here
return em, nil
}
dbPath, err := r.SqlitePath()
if err != nil {
return nil, err
}
transactionHashLookup, err := chain.NewTransactionHashLookup(filepath.Join(dbPath + "txHash.db"))
if err != nil {
return nil, err
}
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
return transactionHashLookup.Close()
},
})
ethTxHashManager := full.EthTxHashManager{
StateAPI: stateapi,
TransactionHashLookup: transactionHashLookup,
}
em.EthTxHashManager = &ethTxHashManager
const ChainHeadConfidence = 1
ctx := helpers.LifecycleCtx(mctx, lc)
lc.Append(fx.Hook{
OnStart: func(context.Context) error {
ev, err := events.NewEventsWithConfidence(ctx, &evapi, ChainHeadConfidence)
if err != nil {
return err
}
// Tipset listener
_ = ev.Observe(&ethTxHashManager)
ch, err := mp.Updates(ctx)
if err != nil {
return err
}
go full.WaitForMpoolUpdates(ctx, ch, &ethTxHashManager)
return nil
},
})
return em, nil
}
}

View File

@ -37,6 +37,7 @@ const (
fsDatastore = "datastore" fsDatastore = "datastore"
fsLock = "repo.lock" fsLock = "repo.lock"
fsKeystore = "keystore" fsKeystore = "keystore"
fsSqlite = "sqlite"
) )
func NewRepoTypeFromString(t string) RepoType { func NewRepoTypeFromString(t string) RepoType {
@ -411,6 +412,10 @@ type fsLockedRepo struct {
ssErr error ssErr error
ssOnce sync.Once ssOnce sync.Once
sqlPath string
sqlErr error
sqlOnce sync.Once
storageLk sync.Mutex storageLk sync.Mutex
configLk sync.Mutex configLk sync.Mutex
} }
@ -515,6 +520,21 @@ func (fsr *fsLockedRepo) SplitstorePath() (string, error) {
return fsr.ssPath, fsr.ssErr return fsr.ssPath, fsr.ssErr
} }
func (fsr *fsLockedRepo) SqlitePath() (string, error) {
fsr.sqlOnce.Do(func() {
path := fsr.join(fsSqlite)
if err := os.MkdirAll(path, 0755); err != nil {
fsr.sqlErr = err
return
}
fsr.sqlPath = path
})
return fsr.sqlPath, fsr.sqlErr
}
// join joins path elements with fsr.path // join joins path elements with fsr.path
func (fsr *fsLockedRepo) join(paths ...string) string { func (fsr *fsLockedRepo) join(paths ...string) string {
return filepath.Join(append([]string{fsr.path}, paths...)...) return filepath.Join(append([]string{fsr.path}, paths...)...)

View File

@ -69,6 +69,9 @@ type LockedRepo interface {
// SplitstorePath returns the path for the SplitStore // SplitstorePath returns the path for the SplitStore
SplitstorePath() (string, error) SplitstorePath() (string, error)
// SqlitePath returns the path for the Sqlite database
SqlitePath() (string, error)
// Returns config in this repo // Returns config in this repo
Config() (interface{}, error) Config() (interface{}, error)
SetConfig(func(interface{})) error SetConfig(func(interface{})) error

View File

@ -277,6 +277,14 @@ func (lmem *lockedMemRepo) SplitstorePath() (string, error) {
return splitstorePath, nil return splitstorePath, nil
} }
func (lmem *lockedMemRepo) SqlitePath() (string, error) {
sqlitePath := filepath.Join(lmem.Path(), "sqlite")
if err := os.MkdirAll(sqlitePath, 0755); err != nil {
return "", err
}
return sqlitePath, nil
}
func (lmem *lockedMemRepo) ListDatastores(ns string) ([]int64, error) { func (lmem *lockedMemRepo) ListDatastores(ns string) ([]int64, error) {
return nil, nil return nil, nil
} }