diff --git a/.circleci/config.yml b/.circleci/config.yml index d3c160cd5..db625b2be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/api/api_full.go b/api/api_full.go index 41fbe6b3e..b17fad3b5 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -778,6 +778,8 @@ 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 + EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, 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 diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 10ff250e6..b32fc7d8b 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1177,6 +1177,21 @@ func (mr *MockFullNodeMockRecorder) EthGetLogs(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetLogs", reflect.TypeOf((*MockFullNode)(nil).EthGetLogs), arg0, arg1) } +// EthGetMessageCidByTransactionHash mocks base method. +func (m *MockFullNode) EthGetMessageCidByTransactionHash(arg0 context.Context, arg1 *ethtypes.EthHash) (*cid.Cid, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthGetMessageCidByTransactionHash", arg0, arg1) + ret0, _ := ret[0].(*cid.Cid) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthGetMessageCidByTransactionHash indicates an expected call of EthGetMessageCidByTransactionHash. +func (mr *MockFullNodeMockRecorder) EthGetMessageCidByTransactionHash(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetMessageCidByTransactionHash", reflect.TypeOf((*MockFullNode)(nil).EthGetMessageCidByTransactionHash), arg0, arg1) +} + // EthGetStorageAt mocks base method. func (m *MockFullNode) EthGetStorageAt(arg0 context.Context, arg1 ethtypes.EthAddress, arg2 ethtypes.EthBytes, arg3 string) (ethtypes.EthBytes, error) { m.ctrl.T.Helper() @@ -1252,6 +1267,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() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 67249bb5f..aaa1d87c7 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -253,6 +253,8 @@ type FullNodeStruct struct { EthGetLogs func(p0 context.Context, p1 *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) `perm:"read"` + EthGetMessageCidByTransactionHash func(p0 context.Context, p1 *ethtypes.EthHash) (*cid.Cid, error) `perm:"read"` + EthGetStorageAt func(p0 context.Context, p1 ethtypes.EthAddress, p2 ethtypes.EthBytes, p3 string) (ethtypes.EthBytes, error) `perm:"read"` EthGetTransactionByBlockHashAndIndex func(p0 context.Context, p1 ethtypes.EthHash, p2 ethtypes.EthUint64) (ethtypes.EthTx, error) `perm:"read"` @@ -263,6 +265,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"` @@ -2117,6 +2121,17 @@ func (s *FullNodeStub) EthGetLogs(p0 context.Context, p1 *ethtypes.EthFilterSpec return nil, ErrNotSupported } +func (s *FullNodeStruct) EthGetMessageCidByTransactionHash(p0 context.Context, p1 *ethtypes.EthHash) (*cid.Cid, error) { + if s.Internal.EthGetMessageCidByTransactionHash == nil { + return nil, ErrNotSupported + } + return s.Internal.EthGetMessageCidByTransactionHash(p0, p1) +} + +func (s *FullNodeStub) EthGetMessageCidByTransactionHash(p0 context.Context, p1 *ethtypes.EthHash) (*cid.Cid, error) { + return nil, ErrNotSupported +} + func (s *FullNodeStruct) EthGetStorageAt(p0 context.Context, p1 ethtypes.EthAddress, p2 ethtypes.EthBytes, p3 string) (ethtypes.EthBytes, error) { if s.Internal.EthGetStorageAt == nil { return *new(ethtypes.EthBytes), ErrNotSupported @@ -2172,6 +2187,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 diff --git a/build/actors/v10.tar.zst b/build/actors/v10.tar.zst index f12e7b251..b0c9e5ce8 100644 Binary files a/build/actors/v10.tar.zst and b/build/actors/v10.tar.zst differ diff --git a/build/builtin_actors_gen.go b/build/builtin_actors_gen.go index 3ae845c16..38d4c49ed 100644 --- a/build/builtin_actors_gen.go +++ b/build/builtin_actors_gen.go @@ -53,14 +53,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "butterflynet", Version: 10, - ManifestCid: MustParseCid("bafy2bzacecjs7xvhtejsh47b2tx2iwe7mbad4kxovbfs7a6wxfl47kcnl25bm"), + ManifestCid: MustParseCid("bafy2bzaced2wq4k4i2deknam6ehbynaoo37bhysud7eze7su3ftlaggwwjuje"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacebd5zetyjtragjwrv2nqktct6u2pmsi4eifbanovxohx3a7lszjxi"), "cron": MustParseCid("bafk2bzacecrszortqkc7har77ssgajglymv6ftrqvmdko5h2yqqh5k2qospl2"), "datacap": MustParseCid("bafk2bzacecapjnxnyw4talwqv5ajbtbkzmzqiosztj5cb3sortyp73ndjl76e"), - "eam": MustParseCid("bafk2bzaceavdyeveel5iohjg7t6twc2cbdo7bt3m5xajwtibekudyhzv2xojy"), + "eam": MustParseCid("bafk2bzacebsvtqzp7g7vpufbyqrwwcpuo2yu3y7kenm7auidyiwzcv6jdw724"), "ethaccount": MustParseCid("bafk2bzacedl4pmkfxkzoqajs6im3ranmopozsmxjcxsnk3kwvd3vv7mfwwrf4"), - "evm": MustParseCid("bafk2bzacebgzvmvwv7rsnnhp3zhqbiqkumvyrc7pazfovpptgpgtqkalrli74"), + "evm": MustParseCid("bafk2bzacedx5wdyaihi22pwqqqtfxmuwh5acem46mzaep3znmhh5bsuqmxogq"), "init": MustParseCid("bafk2bzacecbxp66q3ytjkg37nyv4rmzezbfaigvx4i5yhvqbm5gg4amjeaias"), "multisig": MustParseCid("bafk2bzacecjltag3mn75dsnmrmopjow27buxqhabissowayqlmavrcfetqswc"), "paymentchannel": MustParseCid("bafk2bzacednzxg263eqbl2imwz3uhujov63tjkffieyl4hl3dhrgxyhwep6hc"), @@ -110,14 +110,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "calibrationnet", Version: 10, - ManifestCid: MustParseCid("bafy2bzaceaklxgrzd34i53rm4eeq6477nlq2ckpex27evftbsmjd2yrdbj4ba"), + ManifestCid: MustParseCid("bafy2bzacearpwvmcqlailxyq2d2wtzmtudxqhvfot77tbdqotek5qiq5hyhzg"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacea7zmrdz2rjbzlbmrmx3ko6pm3cbyqxxgogiqldsccbqffuok7m6s"), "cron": MustParseCid("bafk2bzacec7bxugi7ouh75nglycy7qwdq7e2hnku3w6yafq4fwdwvvq2mtrl2"), "datacap": MustParseCid("bafk2bzacedii4stmlo3ccdff7eevcolmgnuxy5ftkzbzwtkqa4iinlfzq4mei"), - "eam": MustParseCid("bafk2bzacea6du2tjdewnfd2zofjp342d2lw7rdl6hx4ejawup744kpym2xsf4"), + "eam": MustParseCid("bafk2bzacedykxiyewqijj5nksr7qi6o4wu5yz4rezb747ntql4rpidyfdpes4"), "ethaccount": MustParseCid("bafk2bzacecgbcbh3uk7olcfdqo44no5nxxayeqnycdznrlekqigbifor2revm"), - "evm": MustParseCid("bafk2bzaceanxhvz5czs6xfunhbysbttmim5e7poftibsu53uqn4by5nqmdaj6"), + "evm": MustParseCid("bafk2bzaceau5n66rabegik55kymni6uyk7n7jb5eymfywybs543yifpl7du2m"), "init": MustParseCid("bafk2bzacea7lxnvgxupwwgoxlmwtrca75w73qabe324wnwx43qranbgf5zdqo"), "multisig": MustParseCid("bafk2bzacear5eu5gpbjlroqkmsgpqerzc4aemp2uqcaeq7s2h4ur4ucgpzesg"), "paymentchannel": MustParseCid("bafk2bzacecwxuruxawcru7xfcx3rmt4hmhlfh4hi6jvfumerazz6jpvfmxxcw"), @@ -176,14 +176,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "caterpillarnet", Version: 10, - ManifestCid: MustParseCid("bafy2bzaceawh5opc4uqctlzc6xnq3pb7ycchfqwprjysbfa5xlrmiicbbvkrm"), + ManifestCid: MustParseCid("bafy2bzacebxr4uvnf5g3373shjzbaca6pf4th6nnfubytjfbrlxcpvbjw4ane"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacedfms6w3ghqtljpgsfuiqa6ztjx7kcuin6myjezj6rypj3zjbqms6"), "cron": MustParseCid("bafk2bzaceaganmlpozvy4jywigs46pfrtdmhjjey6uyhpurplqbasojsislba"), "datacap": MustParseCid("bafk2bzacebafqqe3wv5ytkfwmqzbmchgem66pw6yq6rl7w6vlhqsbkxnisswq"), - "eam": MustParseCid("bafk2bzaceawl3twv7iontkiiwgezkub2vvgd7cprhv7wvgpqjpeh4o6ygshlg"), + "eam": MustParseCid("bafk2bzacedwk5eqczflcsuisqsyeomgkpg54olojjq2ieb2ozu5s45wfwluti"), "ethaccount": MustParseCid("bafk2bzaceburkmtd63nmzxpux5rcxsbqr6x5didl2ce7al32g4tqrvo4pjz2i"), - "evm": MustParseCid("bafk2bzacea7tp4lop7ivhay3ozitkmxxurk74v4zse42ant47rh2uw5z3tq5e"), + "evm": MustParseCid("bafk2bzacedbroioygjnbjtc7ykcjjs4wfbwnaa6gkzubi7c5enifoqqqu66s6"), "init": MustParseCid("bafk2bzaced23r54kwuebl7t6mdantbby5qpfduxwxfryeliof2enyqzhokix6"), "multisig": MustParseCid("bafk2bzacebcn3rib6j6jvclys7dkf62hco45ssgamczkrtzt6xyewd6gt3mtu"), "paymentchannel": MustParseCid("bafk2bzacecvas4leo44pqdguj22nnwqoqdgwajzrpm5d6ltkehc37ni6p6doq"), @@ -233,14 +233,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "devnet", Version: 10, - ManifestCid: MustParseCid("bafy2bzacedfwwsn5weycwkqrnusc37m6ut2uf42z5qvbukl67wi76mqtgafw2"), + ManifestCid: MustParseCid("bafy2bzacebixrjysarwxdadewlllfp4rwfoejxstwdutghghei54uvuuxlsbq"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacebb5txxkfexeaxa2th3rckxsxchzyss3ijgqbicf265h7rre2rvhm"), "cron": MustParseCid("bafk2bzacecotn4gwluhamoqwnzgbg7ogehv26o5xnhjzltnzfv6utrlyanzek"), "datacap": MustParseCid("bafk2bzacea4hket2srrtbewkf3tip6ellwpxdfbrzt5u47y57i2k6iojqqgba"), - "eam": MustParseCid("bafk2bzacecrg5sjpnmk3nu3vqyegkmjnvsjoumptseuu7zabeggu745bd2kwo"), + "eam": MustParseCid("bafk2bzacecxm2gr6tevzzan6oqp6aiqydjm5b7eo34mlzo5jdm7mnlbbueikq"), "ethaccount": MustParseCid("bafk2bzacedh4y3zvtgft3i6ift4rpptgr2dx67pvenowvq7yaspuf25gqgcdc"), - "evm": MustParseCid("bafk2bzacecrjgqoozymyoplrmtpi7bmkmggiqgpbgwkzooy2a67fjivuedm6a"), + "evm": MustParseCid("bafk2bzacec26myls7vg6anr5yjbb2r75dryhdzwlwnrhjcyuhahlaoxdrua6i"), "init": MustParseCid("bafk2bzacedof2ckc6w2qboxzxv4w67njcug4ut4cq3nnlrfybzsvlgnp4kt24"), "multisig": MustParseCid("bafk2bzacec4eqajjqhl53tnkbs7glu7njlbtlditi7lxhvw33ezmxk6jae46y"), "paymentchannel": MustParseCid("bafk2bzacec6nvdprqja7dy3qp5islebbbh2ifiyg2p7arbe6pocjhfe6xwkfy"), @@ -299,14 +299,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "hyperspace", Version: 10, - ManifestCid: MustParseCid("bafy2bzacedimb4dzty5tsyy3ucbcxai7crli452wn5cguhpmuelq74i4bffoo"), + ManifestCid: MustParseCid("bafy2bzaced6hc7ujjmypg6mkrxdmf32oh2udhmhpmwkqyxusdkxoi2uoodyxg"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacecim7uybic2qprbkjhowg7qkniv4zywj5h5g4u4ss72urco2akzuo"), "cron": MustParseCid("bafk2bzaceahgq64awp4f7li3hdgimc4upqvdvltpmeywckvens33umcxt424a"), "datacap": MustParseCid("bafk2bzacebkxn52ttooaslkwncijk3bgd3tm2zw7vijdhwvg2cxnxbrzmmq5e"), - "eam": MustParseCid("bafk2bzacedg5bnw3ic2ub4mb2agrvdowpqd7xyjv6v2ndlkugnrtjgzzfxqlw"), + "eam": MustParseCid("bafk2bzaceaftiqwpx6dcjfqxyq7pazn2p55diukf32pz74755vj7pgg5joexw"), "ethaccount": MustParseCid("bafk2bzacealn5enbxyxbfs7gbsjbyma2zk3bcr7okvflxhpr753d4eh6ixooa"), - "evm": MustParseCid("bafk2bzacedljkrmazyewawpnddrkzrt55556374dw2pm2hokgkompgzw4vx5y"), + "evm": MustParseCid("bafk2bzacea6etsvrqejjl7uej5dxlswja5gxzqyggsjjvg27timvtiedf7nsg"), "init": MustParseCid("bafk2bzacec55gyyaqjrw7zughywocgwcjvv6k5fijjpjw4xgckuqz6pjtff5a"), "multisig": MustParseCid("bafk2bzaceblozbdzybdivvjdiid4jwm2jc6x5a66sunh2vvwsqba6wzqmr7i6"), "paymentchannel": MustParseCid("bafk2bzacealcyke5a6n24efs6qe4iikynpk2twqssyugy7jcyf6p6shgw2iwa"), @@ -356,14 +356,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "mainnet", Version: 10, - ManifestCid: MustParseCid("bafy2bzaceat4ut5xv3qn4lvvkvwvdn6gtlbnqzvueh67fjqlemw6eled5oqnc"), + ManifestCid: MustParseCid("bafy2bzacea5vylkbby7rb42fknkk4g4byhj7hkqlxp4z4urydi3vlpwsgllik"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzacedsn6i2flkpk6sb4iuejo7gfl5n6fhsdawggtbsihlrrjtvs7oepu"), "cron": MustParseCid("bafk2bzacecw4guere7ba2canyi2622lw52b5qbn7iubckcp5cwlmx2kw7qqwy"), "datacap": MustParseCid("bafk2bzaceat2ncckd2jjjqcovd3ib4sylwff7jk7rlk6gr5d2gmrrc7isrmu2"), - "eam": MustParseCid("bafk2bzacebl7267zqf7aubpl7n6ligulayhz65dpgn3ii26b3wwjwytlsdc3i"), + "eam": MustParseCid("bafk2bzacebbpu5smjrjqpkrvvlhcpk23yvlovlndqmwzhfz5kuuph54tdw732"), "ethaccount": MustParseCid("bafk2bzacedmwzkbytxfn7exmxxosomvix4mpyxrmupeqw45aofqmdq5q7mgqe"), - "evm": MustParseCid("bafk2bzacecrrwixyqwphxaybhy5zxuawkhncq5tgtuz2ind4bl22oivzjidoq"), + "evm": MustParseCid("bafk2bzacechkf43lmddynxmc35hvz5kwr3fdxrbg6fxbcvysfsihgiopbrb7o"), "init": MustParseCid("bafk2bzacec6276d7ls3hhuqibqorn3yp45mv7hroczf3bgb6jkhmbb2zqt3bw"), "multisig": MustParseCid("bafk2bzaceahggxrnjj3w3cgtko54srssqyhcs4x6y55ytego6jf2owg5piw3y"), "paymentchannel": MustParseCid("bafk2bzaceaobaqjamso57bkjv3n4ilv7lfropgrncnnej666w3tegmr4cfgve"), @@ -413,14 +413,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "testing", Version: 10, - ManifestCid: MustParseCid("bafy2bzacearh2dy6uzcfvruckai5qbf7banklkg6uezaa7w6onsmdfxn2qxbs"), + ManifestCid: MustParseCid("bafy2bzacea7tbn4p232ecrjvlp2uvpci5pexqjqq2vpv4t5ihktpja2zsj3ek"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzaceds3iy5qjgr3stoywxt4uxvhybca23q7d2kxhitedgudrkhxaxa6o"), "cron": MustParseCid("bafk2bzacebxp4whb4ocqxnbvqlz3kckarabtyvhjbhqvrdwhejuffwactyiss"), "datacap": MustParseCid("bafk2bzacedepm3zas6vqryruwiz7d3axkneo7v66q65gf2dlpfd53pjlycrg4"), - "eam": MustParseCid("bafk2bzacedl4q6l3m5uvunuviwlxicyweszrahipxfpf3nt6wspvcdi4ryzyk"), + "eam": MustParseCid("bafk2bzacea2uascrtv6xnsqlxyf3tcf4onpgrs7frh55p6dnrdeum2uup7wx4"), "ethaccount": MustParseCid("bafk2bzacecbhz4ipg773lsovgpjysm6fxl2i7y2wuxadqnt4s4vm3nd2qodb4"), - "evm": MustParseCid("bafk2bzaced5efc2bi7ulqsep4ej74hxwbjap2qi7lojiqzfsowxr4kylkwzk6"), + "evm": MustParseCid("bafk2bzaceabwn4i62od3i4qkuj5zx4vn5w5cbcl53tqnszk6kl43bfl55hl6c"), "init": MustParseCid("bafk2bzacebqym5i5eciyyyzsimu73z6bkffpm5hzjpx3gwcm64pm2fh7okrja"), "multisig": MustParseCid("bafk2bzacecmlyngek7qvj5ezaaitadrycapup3mbty4ijlzun6g23tcoysxle"), "paymentchannel": MustParseCid("bafk2bzacedspin4hxpgnxkjen3hsxpcc52oc5q4ypukl4qq6vaytcgmmi7hl4"), @@ -470,14 +470,14 @@ var EmbeddedBuiltinActorsMetadata []*BuiltinActorsMetadata = []*BuiltinActorsMet }, { Network: "testing-fake-proofs", Version: 10, - ManifestCid: MustParseCid("bafy2bzacedw6kk3u2vjexzqmm3vtvusd42lllk7wbnsrlxxmt35smsnmiatca"), + ManifestCid: MustParseCid("bafy2bzacecyqfyzmw72234rvbk6vzq2omnmt3cbfezkq2h3ewnn33w42b2s62"), Actors: map[string]cid.Cid{ "account": MustParseCid("bafk2bzaceds3iy5qjgr3stoywxt4uxvhybca23q7d2kxhitedgudrkhxaxa6o"), "cron": MustParseCid("bafk2bzacebxp4whb4ocqxnbvqlz3kckarabtyvhjbhqvrdwhejuffwactyiss"), "datacap": MustParseCid("bafk2bzacedepm3zas6vqryruwiz7d3axkneo7v66q65gf2dlpfd53pjlycrg4"), - "eam": MustParseCid("bafk2bzacedl4q6l3m5uvunuviwlxicyweszrahipxfpf3nt6wspvcdi4ryzyk"), + "eam": MustParseCid("bafk2bzacea2uascrtv6xnsqlxyf3tcf4onpgrs7frh55p6dnrdeum2uup7wx4"), "ethaccount": MustParseCid("bafk2bzacecbhz4ipg773lsovgpjysm6fxl2i7y2wuxadqnt4s4vm3nd2qodb4"), - "evm": MustParseCid("bafk2bzaced5efc2bi7ulqsep4ej74hxwbjap2qi7lojiqzfsowxr4kylkwzk6"), + "evm": MustParseCid("bafk2bzaceabwn4i62od3i4qkuj5zx4vn5w5cbcl53tqnszk6kl43bfl55hl6c"), "init": MustParseCid("bafk2bzacebqym5i5eciyyyzsimu73z6bkffpm5hzjpx3gwcm64pm2fh7okrja"), "multisig": MustParseCid("bafk2bzacecmlyngek7qvj5ezaaitadrycapup3mbty4ijlzun6g23tcoysxle"), "paymentchannel": MustParseCid("bafk2bzacedspin4hxpgnxkjen3hsxpcc52oc5q4ypukl4qq6vaytcgmmi7hl4"), diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 6b4fc7b4f..d7a354461 100644 Binary files a/build/openrpc/full.json.gz and b/build/openrpc/full.json.gz differ diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index cc3870a49..74a9f3221 100644 Binary files a/build/openrpc/gateway.json.gz and b/build/openrpc/gateway.json.gz differ diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index 4f67b5393..d3750150c 100644 Binary files a/build/openrpc/miner.json.gz and b/build/openrpc/miner.json.gz differ diff --git a/build/openrpc/worker.json.gz b/build/openrpc/worker.json.gz index 88e449804..e84f7f5d1 100644 Binary files a/build/openrpc/worker.json.gz and b/build/openrpc/worker.json.gz differ diff --git a/chain/ethhashlookup/eth_transaction_hash_lookup.go b/chain/ethhashlookup/eth_transaction_hash_lookup.go new file mode 100644 index 000000000..85cb84db1 --- /dev/null +++ b/chain/ethhashlookup/eth_transaction_hash_lookup.go @@ -0,0 +1,163 @@ +package ethhashlookup + +import ( + "database/sql" + "errors" + "strconv" + + "github.com/ipfs/go-cid" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/xerrors" + + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +var ErrNotFound = errors.New("not found") + +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 eth_tx_hashes ( + hash TEXT PRIMARY KEY NOT NULL, + cid TEXT NOT NULL UNIQUE, + insertion_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + )`, + + `CREATE INDEX IF NOT EXISTS insertion_time_index ON eth_tx_hashes (insertion_time)`, + + // 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 ( + insertTxHash = `INSERT INTO eth_tx_hashes + (hash, cid) + VALUES(?, ?) + ON CONFLICT (hash) DO UPDATE SET insertion_time = CURRENT_TIMESTAMP` +) + +type EthTxHashLookup struct { + db *sql.DB +} + +func (ei *EthTxHashLookup) UpsertHash(txHash ethtypes.EthHash, c cid.Cid) 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()) + return err +} + +func (ei *EthTxHashLookup) GetCidFromHash(txHash ethtypes.EthHash) (cid.Cid, error) { + q, err := ei.db.Query("SELECT cid FROM eth_tx_hashes WHERE hash = :hash;", sql.Named("hash", txHash.String())) + if err != nil { + return cid.Undef, err + } + + var c string + if !q.Next() { + return cid.Undef, ErrNotFound + } + err = q.Scan(&c) + if err != nil { + return cid.Undef, err + } + return cid.Decode(c) +} + +func (ei *EthTxHashLookup) GetHashFromCid(c cid.Cid) (ethtypes.EthHash, error) { + q, err := ei.db.Query("SELECT hash FROM eth_tx_hashes WHERE cid = :cid;", sql.Named("cid", c.String())) + if err != nil { + return ethtypes.EmptyEthHash, err + } + + var hashString string + if !q.Next() { + return ethtypes.EmptyEthHash, ErrNotFound + } + err = q.Scan(&hashString) + if err != nil { + return ethtypes.EmptyEthHash, err + } + return ethtypes.ParseEthHash(hashString) +} + +func (ei *EthTxHashLookup) DeleteEntriesOlderThan(days int) (int64, error) { + res, err := ei.db.Exec("DELETE FROM eth_tx_hashes WHERE insertion_time < datetime('now', ?);", "-"+strconv.Itoa(days)+" day") + if err != nil { + return 0, err + } + + return res.RowsAffected() +} + +func NewTransactionHashLookup(path string) (*EthTxHashLookup, 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 &EthTxHashLookup{ + db: db, + }, nil +} + +func (ei *EthTxHashLookup) Close() error { + if ei.db == nil { + return nil + } + return ei.db.Close() +} diff --git a/chain/events/filter/mempool.go b/chain/events/filter/mempool.go index 250fc5961..39ccf12c2 100644 --- a/chain/events/filter/mempool.go +++ b/chain/events/filter/mempool.go @@ -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 diff --git a/chain/store/snapshot.go b/chain/store/snapshot.go index f9e65f4bf..3c264d192 100644 --- a/chain/store/snapshot.go +++ b/chain/store/snapshot.go @@ -22,6 +22,8 @@ import ( "github.com/filecoin-project/lotus/chain/types" ) +const TIPSETKEY_BACKFILL_RANGE = 2 * build.Finality + func (cs *ChainStore) UnionStore() bstore.Blockstore { return bstore.Union(cs.stateBlockstore, cs.chainBlockstore) } @@ -113,6 +115,20 @@ func (cs *ChainStore) Import(ctx context.Context, r io.Reader) (*types.TipSet, e return nil, xerrors.Errorf("failed to load root tipset from chainfile: %w", err) } + ts := root + for i := 0; i < int(TIPSETKEY_BACKFILL_RANGE); i++ { + err = cs.PersistTipset(ctx, ts) + if err != nil { + return nil, err + } + parentTsKey := ts.Parents() + ts, err = cs.LoadTipSet(ctx, parentTsKey) + if ts == nil || err != nil { + log.Warnf("Only able to load the last %d tipsets", i) + break + } + } + return root, nil } diff --git a/chain/store/store.go b/chain/store/store.go index 9ab08c74f..5ed037ee5 100644 --- a/chain/store/store.go +++ b/chain/store/store.go @@ -1097,6 +1097,10 @@ func (cs *ChainStore) StateBlockstore() bstore.Blockstore { return cs.stateBlockstore } +func (cs *ChainStore) ChainLocalBlockstore() bstore.Blockstore { + return cs.chainLocalBlockstore +} + func ActorStore(ctx context.Context, bs bstore.Blockstore) adt.Store { return adt.WrapStore(ctx, cbor.NewCborStore(bs)) } diff --git a/chain/store/store_test.go b/chain/store/store_test.go index 6dc340737..89d0caa2c 100644 --- a/chain/store/store_test.go +++ b/chain/store/store_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/ipfs/go-datastore" + "github.com/stretchr/testify/require" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" @@ -124,6 +125,51 @@ func TestChainExportImport(t *testing.T) { } } +// Test to check if tipset key cids are being stored on snapshot +func TestChainImportTipsetKeyCid(t *testing.T) { + + ctx := context.Background() + cg, err := gen.NewGenerator() + require.NoError(t, err) + + buf := new(bytes.Buffer) + var last *types.TipSet + var tsKeys []types.TipSetKey + for i := 0; i < 10; i++ { + ts, err := cg.NextTipSet() + require.NoError(t, err) + last = ts.TipSet.TipSet() + tsKeys = append(tsKeys, last.Key()) + } + + if err := cg.ChainStore().Export(ctx, last, last.Height(), false, buf); err != nil { + t.Fatal(err) + } + + nbs := blockstore.NewMemorySync() + cs := store.NewChainStore(nbs, nbs, datastore.NewMapDatastore(), filcns.Weight, nil) + defer cs.Close() //nolint:errcheck + + root, err := cs.Import(ctx, buf) + require.NoError(t, err) + + require.Truef(t, root.Equals(last), "imported chain differed from exported chain") + + err = cs.SetHead(ctx, last) + require.NoError(t, err) + + for _, tsKey := range tsKeys { + _, err := cs.LoadTipSet(ctx, tsKey) + require.NoError(t, err) + + tsCid, err := tsKey.Cid() + require.NoError(t, err) + _, err = cs.ChainLocalBlockstore().Get(ctx, tsCid) + require.NoError(t, err) + + } +} + func TestChainExportImportFull(t *testing.T) { //stm: @CHAIN_GEN_NEXT_TIPSET_001 //stm: @CHAIN_STORE_IMPORT_001, @CHAIN_STORE_EXPORT_001, @CHAIN_STORE_SET_HEAD_001 diff --git a/chain/types/ethtypes/eth_transactions.go b/chain/types/ethtypes/eth_transactions.go index ba4b14f56..b6ae22169 100644 --- a/chain/types/ethtypes/eth_transactions.go +++ b/chain/types/ethtypes/eth_transactions.go @@ -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 { diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index e869be021..eb0e12891 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -396,10 +396,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") @@ -560,7 +571,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. diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index fe5dd542c..2c853754b 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -83,11 +83,13 @@ * [EthGetFilterChanges](#EthGetFilterChanges) * [EthGetFilterLogs](#EthGetFilterLogs) * [EthGetLogs](#EthGetLogs) + * [EthGetMessageCidByTransactionHash](#EthGetMessageCidByTransactionHash) * [EthGetStorageAt](#EthGetStorageAt) * [EthGetTransactionByBlockHashAndIndex](#EthGetTransactionByBlockHashAndIndex) * [EthGetTransactionByBlockNumberAndIndex](#EthGetTransactionByBlockNumberAndIndex) * [EthGetTransactionByHash](#EthGetTransactionByHash) * [EthGetTransactionCount](#EthGetTransactionCount) + * [EthGetTransactionHashByCid](#EthGetTransactionHashByCid) * [EthGetTransactionReceipt](#EthGetTransactionReceipt) * [EthMaxPriorityFeePerGas](#EthMaxPriorityFeePerGas) * [EthNewBlockFilter](#EthNewBlockFilter) @@ -2640,6 +2642,25 @@ Response: ] ``` +### EthGetMessageCidByTransactionHash + + +Perms: read + +Inputs: +```json +[ + "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e" +] +``` + +Response: +```json +{ + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" +} +``` + ### EthGetStorageAt @@ -2778,6 +2799,22 @@ Inputs: Response: `"0x5"` +### EthGetTransactionHashByCid + + +Perms: read + +Inputs: +```json +[ + { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } +] +``` + +Response: `"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"` + ### EthGetTransactionReceipt diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 7a1ab7bc5..fbdbc852c 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -343,3 +343,19 @@ #ActorEventDatabasePath = "" +[Fevm] + # EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids + # You will not be able to look up ethereum transactions by their hash if this is disabled. + # + # type: bool + # env var: LOTUS_FEVM_ENABLEETHHASHTOFILECOINCIDMAPPING + #EnableEthHashToFilecoinCidMapping = false + + # EthTxHashMappingLifetimeDays the transaction hash lookup database will delete mappings that have been stored for more than x days + # Set to 0 to keep all mappings + # + # type: int + # env var: LOTUS_FEVM_ETHTXHASHMAPPINGLIFETIMEDAYS + #EthTxHashMappingLifetimeDays = 0 + + diff --git a/itests/eth_deploy_test.go b/itests/eth_deploy_test.go index 13a68ce46..f73076d02 100644 --- a/itests/eth_deploy_test.go +++ b/itests/eth_deploy_test.go @@ -41,6 +41,7 @@ func TestDeployment(t *testing.T) { cfg.ActorEvent.EnableRealTimeFilterAPI = true return nil }), + kit.EthTxHashLookup(), ) ens.InterconnectAll().BeginMining(blockTime) diff --git a/itests/eth_filter_test.go b/itests/eth_filter_test.go index 6c42b45bd..e4f2a10a1 100644 --- a/itests/eth_filter_test.go +++ b/itests/eth_filter_test.go @@ -294,7 +294,7 @@ func TestEthNewFilterCatchAll(t *testing.T) { kit.QuietAllLogsExcept("events", "messagepool") 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) @@ -1436,9 +1436,9 @@ func invokeAndWaitUntilAllOnChain(t *testing.T, client *kit.TestFullNode, invoca require.True(ok) m.events = evs - 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(len(invocations), len(received), "all messages on chain") diff --git a/itests/eth_hash_lookup_test.go b/itests/eth_hash_lookup_test.go new file mode 100644 index 000000000..bad705fe1 --- /dev/null +++ b/itests/eth_hash_lookup_test.go @@ -0,0 +1,598 @@ +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.hex") + 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.Fevm.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.hex") + 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 +} + +// TestTransactionHashLookupSecpFilecoinMessage tests to see if lotus can find a Secp Filecoin Message using the transaction hash +func TestTransactionHashLookupNonexistentMessage(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() + + cid := build.MustParseCid("bafk2bzacecapjnxnyw4talwqv5ajbtbkzmzqiosztj5cb3sortyp73ndjl76e") + + // We shouldn't be able to return a hash for this fake cid + chainHash, err := client.EthGetTransactionHashByCid(ctx, cid) + require.NoError(t, err) + require.Nil(t, chainHash) + + calculatedHash, err := ethtypes.EthHashFromCid(cid) + require.NoError(t, err) + + // We shouldn't be able to return a cid for this fake hash + chainCid, err := client.EthGetMessageCidByTransactionHash(ctx, &calculatedHash) + require.NoError(t, err) + require.Nil(t, chainCid) +} + +func TestEthGetMessageCidByTransactionHashEthTx(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.hex") + 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) + + sender, err := tx.Sender() + require.NoError(t, err) + + unsignedMessage, err := tx.ToUnsignedMessage(sender) + require.NoError(t, err) + + rawTxHash, err := tx.TxHash() + require.NoError(t, err) + + hash := client.EVM().SubmitTransaction(ctx, &tx) + require.Equal(t, rawTxHash, hash) + + mpoolCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, mpoolCid) + + mpoolTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, mpoolTx) + require.Equal(t, *unsignedMessage, *mpoolTx) + + // 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) + + chainCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, chainCid) + + chainTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, chainTx) + require.Equal(t, *unsignedMessage, *chainTx) +} + +func TestEthGetMessageCidByTransactionHashSecp(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) + + mpoolCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, mpoolCid) + + mpoolTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, mpoolTx) + require.Equal(t, secpSmsg.Message, *mpoolTx) + + _, err = client.StateWaitMsg(ctx, secpSmsg.Cid(), 3, api.LookbackNoLimit, true) + require.NoError(t, err) + + chainCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, chainCid) + + chainTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, chainTx) + require.Equal(t, secpSmsg.Message, *chainTx) +} + +func TestEthGetMessageCidByTransactionHashBLS(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.Cid()) + require.NoError(t, err) + + mpoolCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, mpoolCid) + + mpoolTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, mpoolTx) + require.Equal(t, sm.Message, *mpoolTx) + + _, err = client.StateWaitMsg(ctx, sm.Cid(), 3, api.LookbackNoLimit, true) + require.NoError(t, err) + + chainCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash) + require.NoError(t, err) + require.NotNil(t, chainCid) + + chainTx, err := client.ChainGetMessage(ctx, *mpoolCid) + require.NoError(t, err) + require.NotNil(t, chainTx) + require.Equal(t, sm.Message, *chainTx) +} diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 0c8f1baa5..052aae3cf 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -21,7 +21,7 @@ import ( func TestValueTransferValidSignature(t *testing.T) { blockTime := 100 * time.Millisecond - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.EthTxHashLookup()) ens.InterconnectAll().BeginMining(blockTime) @@ -106,7 +106,7 @@ func TestLegacyTransaction(t *testing.T) { func TestContractDeploymentValidSignature(t *testing.T) { blockTime := 100 * time.Millisecond - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.EthTxHashLookup()) ens.InterconnectAll().BeginMining(blockTime) @@ -167,7 +167,7 @@ func TestContractDeploymentValidSignature(t *testing.T) { func TestContractInvocation(t *testing.T) { blockTime := 100 * time.Millisecond - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.EthTxHashLookup()) ens.InterconnectAll().BeginMining(blockTime) diff --git a/itests/kit/node_opts.go b/itests/kit/node_opts.go index a220a0d1b..efaed8861 100644 --- a/itests/kit/node_opts.go +++ b/itests/kit/node_opts.go @@ -296,3 +296,10 @@ func HistoricFilterAPI(dbpath string) NodeOpt { return nil }) } + +func EthTxHashLookup() NodeOpt { + return WithCfgOpt(func(cfg *config.FullNode) error { + cfg.Fevm.EnableEthHashToFilecoinCidMapping = true + return nil + }) +} diff --git a/node/builder_chain.go b/node/builder_chain.go index 541b451b7..221150be1 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -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.Fevm)), ) } diff --git a/node/config/def.go b/node/config/def.go index 7540aa3f7..12efc408f 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -107,6 +107,10 @@ func DefaultFullNode() *FullNode { MaxFilterResults: 10000, MaxFilterHeightRange: 2880, // conservative limit of one day }, + Fevm: FevmConfig{ + EnableEthHashToFilecoinCidMapping: false, + EthTxHashMappingLifetimeDays: 0, + }, } } diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 0da9c7853..c4cf08471 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -399,6 +399,22 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/# Comment: ``, }, }, + "FevmConfig": []DocField{ + { + Name: "EnableEthHashToFilecoinCidMapping", + Type: "bool", + + Comment: `EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids +You will not be able to look up ethereum transactions by their hash if this is disabled.`, + }, + { + Name: "EthTxHashMappingLifetimeDays", + Type: "int", + + Comment: `EthTxHashMappingLifetimeDays the transaction hash lookup database will delete mappings that have been stored for more than x days +Set to 0 to keep all mappings`, + }, + }, "FullNode": []DocField{ { Name: "Client", @@ -434,6 +450,12 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/# Name: "ActorEvent", Type: "ActorEventConfig", + Comment: ``, + }, + { + Name: "Fevm", + Type: "FevmConfig", + Comment: ``, }, }, diff --git a/node/config/types.go b/node/config/types.go index b0f9b63c0..38671929d 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -28,6 +28,7 @@ type FullNode struct { Chainstore Chainstore Cluster UserRaftConfig ActorEvent ActorEventConfig + Fevm FevmConfig } // // Common @@ -692,3 +693,12 @@ type ActorEventConfig struct { // Set a timeout for subscription clients // Set upper bound on index size } + +type FevmConfig struct { + // EnableEthHashToFilecoinCidMapping enables storing a mapping of eth transaction hashes to filecoin message Cids + // You will not be able to look up ethereum transactions by their hash if this is disabled. + EnableEthHashToFilecoinCidMapping bool + // EthTxHashMappingLifetimeDays the transaction hash lookup database will delete mappings that have been stored for more than x days + // Set to 0 to keep all mappings + EthTxHashMappingLifetimeDays int +} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 2ce3d1a09..009da817c 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -21,11 +21,13 @@ 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/actors" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/ethhashlookup" "github.com/filecoin-project/lotus/chain/events/filter" "github.com/filecoin-project/lotus/chain/messagepool" "github.com/filecoin-project/lotus/chain/stmgr" @@ -43,6 +45,8 @@ 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) + EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, 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 +111,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 + Chain *store.ChainStore + Mpool *messagepool.MessagePool + StateManager *stmgr.StateManager + EthTxHashManager *EthTxHashManager ChainAPI MpoolAPI @@ -254,10 +257,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.GetCidFromHash(*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 +288,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 +300,56 @@ func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtype return nil, nil } +func (a *EthModule) EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) { + // Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid + if txHash == nil { + return nil, nil + } + + c := cid.Undef + if a.EthTxHashManager != nil { + var err error + c, err = a.EthTxHashManager.TransactionHashLookup.GetCidFromHash(*txHash) + // We fall out of the first condition and continue + if errors.Is(err, ethhashlookup.ErrNotFound) { + log.Debug("could not find transaction hash %s in lookup table", txHash.String()) + } else if err != nil { + return nil, xerrors.Errorf("database error: %w", err) + } else { + return &c, nil + } + } + // This isn't an eth transaction we have the mapping for, so let's try looking it up as a filecoin message + if c == cid.Undef { + c = txHash.ToCid() + } + + _, err := a.StateAPI.Chain.GetSignedMessage(ctx, c) + if err == nil { + // This is an Eth Tx, Secp message, Or BLS message in the mpool + return &c, nil + } + + _, err = a.StateAPI.Chain.GetMessage(ctx, c) + if err == nil { + // This is a BLS message + return &c, nil + } + + // Ethereum clients expect an empty response when the message was not found + return nil, nil +} + +func (a *EthModule) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { + hash, err := EthTxHashFromFilecoinMessageCid(ctx, cid, a.StateAPI) + if hash == ethtypes.EmptyEthHash { + // not found + return nil, nil + } + + 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,10 +369,21 @@ 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() + c := cid.Undef + if a.EthTxHashManager != nil { + var err error + c, err = a.EthTxHashManager.TransactionHashLookup.GetCidFromHash(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, cid, api.LookbackNoLimit, true) - if err != nil { + msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, api.LookbackNoLimit, true) + if err != nil || msgLookup == nil { return nil, nil } @@ -317,7 +392,7 @@ func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash ethtype 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 +715,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 +867,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 +882,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 +904,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") @@ -1146,14 +1222,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{ @@ -1179,11 +1255,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 @@ -1218,11 +1293,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 } @@ -1321,7 +1396,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 { @@ -1396,18 +1471,15 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx } gasUsed += msgLookup.Receipt.GasUsed + tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, txIdx, cs, sa) + if err != nil { + return ethtypes.EthBlock{}, nil + } + if fullTxInfo { - tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, txIdx, cs, sa) - if err != nil { - return ethtypes.EthBlock{}, nil - } 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()) } } @@ -1459,19 +1531,42 @@ 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 { + // This is an Eth Tx, Secp message, Or BLS message in the mpool + return EthTxHashFromSignedFilecoinMessage(ctx, smsg, sa) } - toEthAddr, err := lookupEthAddress(ctx, smsg.Message.To, sa) - if err != nil { - return ethtypes.EthTx{}, err + _, err = sa.Chain.GetMessage(ctx, c) + if err == nil { + // This is a BLS message + return ethtypes.EthHashFromCid(c) } + return ethtypes.EmptyEthHash, nil +} + +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.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 { @@ -1510,26 +1605,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 @@ -1542,11 +1649,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 { @@ -1588,10 +1690,21 @@ func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLo smsg, err := cs.GetSignedMessage(ctx, msgLookup.Message) if err != nil { - return ethtypes.EthTx{}, err + // 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 } @@ -1602,7 +1715,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 @@ -1706,6 +1818,84 @@ 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.UpsertHash(hash, smsg.Cid()) + if err != nil { + return err + } + } + } + + return nil +} + +type EthTxHashManager struct { + StateAPI StateAPI + TransactionHashLookup *ethhashlookup.EthTxHashLookup +} + +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.UpsertHash(ethTx.Hash, u.Message.Cid()) + if err != nil { + log.Errorf("error inserting tx mapping to db: %s", err) + } + } + } +} + +func EthTxHashGC(ctx context.Context, retentionDays int, manager *EthTxHashManager) { + if retentionDays == 0 { + return + } + + gcPeriod := 1 * time.Hour + for { + entriesDeleted, err := manager.TransactionHashLookup.DeleteEntriesOlderThan(retentionDays) + if err != nil { + log.Errorf("error garbage collecting eth transaction hash database: %s", err) + } + log.Info("garbage collection run on eth transaction hash lookup database. %d entries deleted", entriesDeleted) + time.Sleep(gcPeriod) + } +} + // decodeLogBytes decodes a CBOR-serialized array into its original form. // // This function swallows errors and returns the original array if it failed diff --git a/node/modules/ethmodule.go b/node/modules/ethmodule.go new file mode 100644 index 000000000..904d09421 --- /dev/null +++ b/node/modules/ethmodule.go @@ -0,0 +1,85 @@ +package modules + +import ( + "context" + "path/filepath" + + "go.uber.org/fx" + + "github.com/filecoin-project/lotus/chain/ethhashlookup" + "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.FevmConfig) 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 := ethhashlookup.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) + go full.EthTxHashGC(ctx, cfg.EthTxHashMappingLifetimeDays, ðTxHashManager) + + return nil + }, + }) + + return em, nil + } +} diff --git a/node/repo/fsrepo.go b/node/repo/fsrepo.go index 68550e389..bd387babf 100644 --- a/node/repo/fsrepo.go +++ b/node/repo/fsrepo.go @@ -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...)...) diff --git a/node/repo/interface.go b/node/repo/interface.go index dd0839559..328862b92 100644 --- a/node/repo/interface.go +++ b/node/repo/interface.go @@ -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 diff --git a/node/repo/memrepo.go b/node/repo/memrepo.go index 61d960872..7817776a9 100644 --- a/node/repo/memrepo.go +++ b/node/repo/memrepo.go @@ -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 }