Store mapping from hashes for Ethereum transactions to Filecoin Message Cids
This commit is contained in:
parent
b6eb7fcd96
commit
a8436074a6
@ -740,6 +740,11 @@ workflows:
|
||||
suite: itest-eth_filter
|
||||
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:
|
||||
name: test-itest-eth_transactions
|
||||
suite: itest-eth_transactions
|
||||
|
@ -778,6 +778,7 @@ type FullNode interface {
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockFullNode) EthGetTransactionReceipt(arg0 context.Context, arg1 ethtypes.EthHash) (*api.EthTxReceipt, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -263,6 +263,8 @@ type FullNodeStruct struct {
|
||||
|
||||
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"`
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if s.Internal.EthGetTransactionReceipt == nil {
|
||||
return nil, ErrNotSupported
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
162
chain/eth_transaction_hash_lookup.go
Normal file
162
chain/eth_transaction_hash_lookup.go
Normal 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()
|
||||
}
|
@ -5,7 +5,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
@ -18,7 +17,7 @@ type MemPoolFilter struct {
|
||||
ch chan<- interface{}
|
||||
|
||||
mu sync.Mutex
|
||||
collected []cid.Cid
|
||||
collected []*types.SignedMessage
|
||||
lastTaken time.Time
|
||||
}
|
||||
|
||||
@ -55,10 +54,10 @@ func (f *MemPoolFilter) CollectMessage(ctx context.Context, msg *types.SignedMes
|
||||
copy(f.collected, 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()
|
||||
collected := f.collected
|
||||
f.collected = nil
|
||||
|
@ -231,6 +231,36 @@ func (tx *EthTxArgs) ToRlpUnsignedMsg() ([]byte, error) {
|
||||
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 (ðTxArgs).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) {
|
||||
packed1, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
|
@ -392,10 +392,21 @@ func ParseEthHash(s string) (EthHash, error) {
|
||||
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 {
|
||||
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 {
|
||||
// err is always nil
|
||||
mh, _ := multihash.EncodeName(h[:], "blake2b-256")
|
||||
@ -556,7 +567,7 @@ type EthLog struct {
|
||||
// The index corresponds to the sequence of messages produced by ChainGetParentMessages
|
||||
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"`
|
||||
|
||||
// BlockHash is the hash of the tipset containing the message that produced the log.
|
||||
|
@ -88,6 +88,7 @@
|
||||
* [EthGetTransactionByBlockNumberAndIndex](#EthGetTransactionByBlockNumberAndIndex)
|
||||
* [EthGetTransactionByHash](#EthGetTransactionByHash)
|
||||
* [EthGetTransactionCount](#EthGetTransactionCount)
|
||||
* [EthGetTransactionHashByCid](#EthGetTransactionHashByCid)
|
||||
* [EthGetTransactionReceipt](#EthGetTransactionReceipt)
|
||||
* [EthMaxPriorityFeePerGas](#EthMaxPriorityFeePerGas)
|
||||
* [EthNewBlockFilter](#EthNewBlockFilter)
|
||||
@ -2778,6 +2779,22 @@ Inputs:
|
||||
|
||||
Response: `"0x5"`
|
||||
|
||||
### EthGetTransactionHashByCid
|
||||
|
||||
|
||||
Perms: read
|
||||
|
||||
Inputs:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Response: `"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"`
|
||||
|
||||
### EthGetTransactionReceipt
|
||||
|
||||
|
||||
|
@ -343,3 +343,11 @@
|
||||
#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
2
extern/filecoin-ffi
vendored
@ -1 +1 @@
|
||||
Subproject commit 86eac2161f442945bffee3fbfe7d094c20b48dd3
|
||||
Subproject commit c4adeb4532719acf7b1c182cb98a3cca7b955a14
|
@ -41,6 +41,7 @@ func TestDeployment(t *testing.T) {
|
||||
cfg.ActorEvent.EnableRealTimeFilterAPI = true
|
||||
return nil
|
||||
}),
|
||||
kit.EthTxHashLookup(),
|
||||
)
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
|
@ -204,7 +204,7 @@ func TestEthNewFilterCatchAll(t *testing.T) {
|
||||
kit.QuietMiningLogs()
|
||||
|
||||
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)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
@ -289,9 +289,9 @@ func TestEthNewFilterCatchAll(t *testing.T) {
|
||||
|
||||
received := make(map[ethtypes.EthHash]msgInTipset)
|
||||
for m := range msgChan {
|
||||
eh, err := ethtypes.EthHashFromCid(m.msg.Cid)
|
||||
eh, err := client.EthGetTransactionHashByCid(ctx, m.msg.Cid)
|
||||
require.NoError(err)
|
||||
received[eh] = m
|
||||
received[*eh] = m
|
||||
}
|
||||
require.Equal(iterations, len(received), "all messages on chain")
|
||||
|
||||
|
340
itests/eth_hash_lookup_test.go
Normal file
340
itests/eth_hash_lookup_test.go
Normal 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: ðAddr,
|
||||
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: ðAddr,
|
||||
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
|
||||
}
|
@ -296,3 +296,10 @@ func HistoricFilterAPI(dbpath string) NodeOpt {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func EthTxHashLookup() NodeOpt {
|
||||
return WithCfgOpt(func(cfg *config.FullNode) error {
|
||||
cfg.EthTxHashConfig.EnableEthHashToFilecoinCidMapping = true
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -161,7 +161,6 @@ var ChainNode = Options(
|
||||
Override(new(messagepool.Provider), messagepool.NewProvider),
|
||||
Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))),
|
||||
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.MpoolModuleAPI), From(new(full.MpoolModule))),
|
||||
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))),
|
||||
// in lite-mode Eth event api is provided by gateway
|
||||
ApplyIf(isFullNode, Override(new(full.EthEventAPI), modules.EthEventAPI(cfg.ActorEvent))),
|
||||
|
||||
Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.EthTxHashConfig)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,9 @@ func DefaultFullNode() *FullNode {
|
||||
MaxFilterResults: 10000,
|
||||
MaxFilterHeightRange: 2880, // conservative limit of one day
|
||||
},
|
||||
EthTxHashConfig: EthTxHashConfig{
|
||||
EnableEthHashToFilecoinCidMapping: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -391,6 +391,14 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
|
||||
Comment: ``,
|
||||
},
|
||||
},
|
||||
"EthTxHashConfig": []DocField{
|
||||
{
|
||||
Name: "EnableEthHashToFilecoinCidMapping",
|
||||
Type: "bool",
|
||||
|
||||
Comment: `EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids`,
|
||||
},
|
||||
},
|
||||
"FeeConfig": []DocField{
|
||||
{
|
||||
Name: "DefaultMaxFee",
|
||||
@ -434,6 +442,12 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
|
||||
Name: "ActorEvent",
|
||||
Type: "ActorEventConfig",
|
||||
|
||||
Comment: ``,
|
||||
},
|
||||
{
|
||||
Name: "EthTxHashConfig",
|
||||
Type: "EthTxHashConfig",
|
||||
|
||||
Comment: ``,
|
||||
},
|
||||
},
|
||||
|
@ -28,6 +28,7 @@ type FullNode struct {
|
||||
Chainstore Chainstore
|
||||
Cluster UserRaftConfig
|
||||
ActorEvent ActorEventConfig
|
||||
EthTxHashConfig EthTxHashConfig
|
||||
}
|
||||
|
||||
// // Common
|
||||
@ -692,3 +693,8 @@ type ActorEventConfig struct {
|
||||
// Set a timeout for subscription clients
|
||||
// Set upper bound on index size
|
||||
}
|
||||
|
||||
type EthTxHashConfig struct {
|
||||
// EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids
|
||||
EnableEthHashToFilecoinCidMapping bool
|
||||
}
|
||||
|
@ -21,9 +21,11 @@ import (
|
||||
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/evm"
|
||||
"github.com/filecoin-project/go-state-types/crypto"
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain"
|
||||
"github.com/filecoin-project/lotus/chain/actors"
|
||||
builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin"
|
||||
"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)
|
||||
EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, 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)
|
||||
EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, 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
|
||||
// within the HEAD tipset.
|
||||
type EthModule struct {
|
||||
fx.In
|
||||
|
||||
Chain *store.ChainStore
|
||||
Mpool *messagepool.MessagePool
|
||||
StateManager *stmgr.StateManager
|
||||
EthTxHashManager *EthTxHashManager
|
||||
|
||||
ChainAPI
|
||||
MpoolAPI
|
||||
@ -254,10 +256,21 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype
|
||||
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
|
||||
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 {
|
||||
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI)
|
||||
if err == nil {
|
||||
@ -274,8 +287,8 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype
|
||||
}
|
||||
|
||||
for _, p := range pending {
|
||||
if p.Cid() == cid {
|
||||
tx, err := newEthTxFromFilecoinMessage(ctx, p, a.StateAPI)
|
||||
if p.Cid() == c {
|
||||
tx, err := NewEthTxFromFilecoinMessage(ctx, p, a.StateAPI)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
addr, err := sender.ToFilecoinAddress()
|
||||
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) {
|
||||
cid := txHash.ToCid()
|
||||
|
||||
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, cid, api.LookbackNoLimit, true)
|
||||
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()
|
||||
}
|
||||
|
||||
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, api.LookbackNoLimit, true)
|
||||
if err != nil || msgLookup == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI)
|
||||
if err != nil {
|
||||
if err != nil && tx.Hash == ethtypes.EmptyEthHash {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
replay, err := a.StateAPI.StateReplay(ctx, types.EmptyTSK, cid)
|
||||
replay, err := a.StateAPI.StateReplay(ctx, types.EmptyTSK, c)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -640,11 +669,12 @@ func (a *EthModule) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.Et
|
||||
smsg.Message.Method = builtinactors.MethodSend
|
||||
}
|
||||
|
||||
cid, err := a.MpoolAPI.MpoolPush(ctx, smsg)
|
||||
_, err = a.MpoolAPI.MpoolPush(ctx, smsg)
|
||||
if err != nil {
|
||||
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) {
|
||||
@ -791,7 +821,7 @@ func (e *EthEvent) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilte
|
||||
|
||||
_ = 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) {
|
||||
@ -806,11 +836,11 @@ func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilte
|
||||
|
||||
switch fc := f.(type) {
|
||||
case filterEventCollector:
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI)
|
||||
case filterTipSetCollector:
|
||||
return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx))
|
||||
case filterMessageCollector:
|
||||
return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx))
|
||||
return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx), e.SubManager.StateAPI)
|
||||
}
|
||||
|
||||
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) {
|
||||
case filterEventCollector:
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI)
|
||||
}
|
||||
|
||||
return nil, xerrors.Errorf("wrong filter type")
|
||||
@ -1141,14 +1171,14 @@ type filterEventCollector interface {
|
||||
}
|
||||
|
||||
type filterMessageCollector interface {
|
||||
TakeCollectedMessages(context.Context) []cid.Cid
|
||||
TakeCollectedMessages(context.Context) []*types.SignedMessage
|
||||
}
|
||||
|
||||
type filterTipSetCollector interface {
|
||||
TakeCollectedTipSets(context.Context) []types.TipSetKey
|
||||
}
|
||||
|
||||
func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*ethtypes.EthFilterResult, error) {
|
||||
func ethFilterResultFromEvents(evs []*filter.CollectedEvent, sa StateAPI) (*ethtypes.EthFilterResult, error) {
|
||||
res := ðtypes.EthFilterResult{}
|
||||
for _, ev := range evs {
|
||||
log := ethtypes.EthLog{
|
||||
@ -1174,11 +1204,10 @@ func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*ethtypes.EthFilte
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.TransactionHash, err = ethtypes.EthHashFromCid(ev.MsgCid)
|
||||
log.TransactionHash, err = EthTxHashFromFilecoinMessageCid(context.TODO(), ev.MsgCid, sa)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := ev.TipSetKey.Cid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1213,11 +1242,11 @@ func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*ethtypes.EthFilterResu
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ethFilterResultFromMessages(cs []cid.Cid) (*ethtypes.EthFilterResult, error) {
|
||||
func ethFilterResultFromMessages(cs []*types.SignedMessage, sa StateAPI) (*ethtypes.EthFilterResult, error) {
|
||||
res := ðtypes.EthFilterResult{}
|
||||
|
||||
for _, c := range cs {
|
||||
hash, err := ethtypes.EthHashFromCid(c)
|
||||
hash, err := EthTxHashFromSignedFilecoinMessage(context.TODO(), c, sa)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1316,7 +1345,7 @@ func (e *ethSubscription) start(ctx context.Context) {
|
||||
var err error
|
||||
switch vt := v.(type) {
|
||||
case *filter.CollectedEvent:
|
||||
resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt})
|
||||
resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt}, e.StateAPI)
|
||||
case *types.TipSet:
|
||||
eb, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.ChainAPI, e.StateAPI)
|
||||
if err != nil {
|
||||
@ -1391,18 +1420,15 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx
|
||||
}
|
||||
gasUsed += msgLookup.Receipt.GasUsed
|
||||
|
||||
if fullTxInfo {
|
||||
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, txIdx, cs, sa)
|
||||
if err != nil {
|
||||
return ethtypes.EthBlock{}, nil
|
||||
}
|
||||
|
||||
if fullTxInfo {
|
||||
block.Transactions = append(block.Transactions, tx)
|
||||
} else {
|
||||
hash, err := ethtypes.EthHashFromCid(msg.Cid())
|
||||
if err != nil {
|
||||
return ethtypes.EthBlock{}, err
|
||||
}
|
||||
block.Transactions = append(block.Transactions, hash.String())
|
||||
block.Transactions = append(block.Transactions, tx.Hash.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1454,19 +1480,35 @@ func lookupEthAddress(ctx context.Context, addr address.Address, sa StateAPI) (e
|
||||
return ethtypes.EthAddressFromFilecoinAddress(idAddr)
|
||||
}
|
||||
|
||||
func newEthTxFromFilecoinMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthTx, error) {
|
||||
fromEthAddr, err := lookupEthAddress(ctx, smsg.Message.From, sa)
|
||||
if err != nil {
|
||||
return ethtypes.EthTx{}, err
|
||||
func EthTxHashFromFilecoinMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) {
|
||||
smsg, err := sa.Chain.GetSignedMessage(ctx, c)
|
||||
if err == nil {
|
||||
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 {
|
||||
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
|
||||
input := smsg.Message.Params
|
||||
var err error
|
||||
// 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).
|
||||
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
|
||||
}
|
||||
|
||||
hash, err := ethtypes.EthHashFromCid(smsg.Cid())
|
||||
if err != nil {
|
||||
return ethtypes.EthTx{}, err
|
||||
}
|
||||
|
||||
tx := ethtypes.EthTx{
|
||||
Hash: hash,
|
||||
Nonce: ethtypes.EthUint64(smsg.Message.Nonce),
|
||||
ChainID: ethtypes.EthUint64(build.Eip155ChainId),
|
||||
From: fromEthAddr,
|
||||
To: toAddr,
|
||||
Value: ethtypes.EthBigInt(smsg.Message.Value),
|
||||
Type: ethtypes.EthUint64(2),
|
||||
Input: input,
|
||||
Gas: ethtypes.EthUint64(smsg.Message.GasLimit),
|
||||
MaxFeePerGas: ethtypes.EthBigInt(smsg.Message.GasFeeCap),
|
||||
MaxPriorityFeePerGas: ethtypes.EthBigInt(smsg.Message.GasPremium),
|
||||
V: v,
|
||||
R: r,
|
||||
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
|
||||
@ -1537,11 +1591,6 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
|
||||
if msgLookup == nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -1582,11 +1631,22 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return ethtypes.EthTx{}, err
|
||||
}
|
||||
@ -1597,7 +1657,6 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo
|
||||
)
|
||||
|
||||
tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId)
|
||||
tx.Hash = txHash
|
||||
tx.BlockHash = &blkHash
|
||||
tx.BlockNumber = &bn
|
||||
tx.TransactionIndex = &ti
|
||||
@ -1701,6 +1760,68 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLook
|
||||
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.
|
||||
//
|
||||
// This function swallows errors and returns the original array if it failed
|
||||
|
84
node/modules/ethmodule.go
Normal file
84
node/modules/ethmodule.go
Normal 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 = ðTxHashManager
|
||||
|
||||
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(ðTxHashManager)
|
||||
|
||||
ch, err := mp.Updates(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go full.WaitForMpoolUpdates(ctx, ch, ðTxHashManager)
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
return em, nil
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ const (
|
||||
fsDatastore = "datastore"
|
||||
fsLock = "repo.lock"
|
||||
fsKeystore = "keystore"
|
||||
fsSqlite = "sqlite"
|
||||
)
|
||||
|
||||
func NewRepoTypeFromString(t string) RepoType {
|
||||
@ -411,6 +412,10 @@ type fsLockedRepo struct {
|
||||
ssErr error
|
||||
ssOnce sync.Once
|
||||
|
||||
sqlPath string
|
||||
sqlErr error
|
||||
sqlOnce sync.Once
|
||||
|
||||
storageLk sync.Mutex
|
||||
configLk sync.Mutex
|
||||
}
|
||||
@ -515,6 +520,21 @@ func (fsr *fsLockedRepo) SplitstorePath() (string, error) {
|
||||
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
|
||||
func (fsr *fsLockedRepo) join(paths ...string) string {
|
||||
return filepath.Join(append([]string{fsr.path}, paths...)...)
|
||||
|
@ -69,6 +69,9 @@ type LockedRepo interface {
|
||||
// SplitstorePath returns the path for the SplitStore
|
||||
SplitstorePath() (string, error)
|
||||
|
||||
// SqlitePath returns the path for the Sqlite database
|
||||
SqlitePath() (string, error)
|
||||
|
||||
// Returns config in this repo
|
||||
Config() (interface{}, error)
|
||||
SetConfig(func(interface{})) error
|
||||
|
@ -277,6 +277,14 @@ func (lmem *lockedMemRepo) SplitstorePath() (string, error) {
|
||||
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) {
|
||||
return nil, nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user