diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index a79238bf8..e869be021 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -36,10 +36,7 @@ var ErrInvalidAddress = errors.New("invalid Filecoin Eth address") type EthUint64 uint64 func (e EthUint64) MarshalJSON() ([]byte, error) { - if e == 0 { - return json.Marshal("0x0") - } - return json.Marshal(fmt.Sprintf("0x%x", e)) + return json.Marshal(e.Hex()) } func (e *EthUint64) UnmarshalJSON(b []byte) error { @@ -64,6 +61,13 @@ func EthUint64FromHex(s string) (EthUint64, error) { return EthUint64(parsedInt), nil } +func (e EthUint64) Hex() string { + if e == 0 { + return "0x0" + } + return fmt.Sprintf("0x%x", e) +} + // EthBigInt represents a large integer whose zero value serializes to "0x0". type EthBigInt big.Int diff --git a/itests/contracts/EventMatrix.hex b/itests/contracts/EventMatrix.hex new file mode 100644 index 000000000..2b3ad91ad --- /dev/null +++ b/itests/contracts/EventMatrix.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506105eb806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c8063c755553811610071578063c755553814610198578063cbfc3b58146101c6578063cc6f8faf14610212578063cd5b6c3d14610254578063e2a614731461028c578063fb62b28b146102d8576100a9565b80630919b8be146100ae5780636199074d146100e657806366eef3461461012857806375091b1f14610132578063a63ae81a1461016a575b600080fd5b6100e4600480360360408110156100c457600080fd5b81019080803590602001909291908035906020019092919050505061031a565b005b610126600480360360608110156100fc57600080fd5b8101908080359060200190929190803590602001909291908035906020019092919050505061035d565b005b610130610391565b005b6101686004803603604081101561014857600080fd5b8101908080359060200190929190803590602001909291905050506103bf565b005b6101966004803603602081101561018057600080fd5b81019080803590602001909291905050506103fb565b005b6101c4600480360360208110156101ae57600080fd5b8101908080359060200190929190505050610435565b005b610210600480360360808110156101dc57600080fd5b8101908080359060200190929190803590602001909291908035906020019092919080359060200190929190505050610465565b005b6102526004803603606081101561022857600080fd5b810190808035906020019092919080359060200190929190803590602001909291905050506104ba565b005b61028a6004803603604081101561026a57600080fd5b8101908080359060200190929190803590602001909291905050506104f8565b005b6102d6600480360360808110156102a257600080fd5b810190808035906020019092919080359060200190929190803590602001909291908035906020019092919050505061052a565b005b610318600480360360608110156102ee57600080fd5b8101908080359060200190929190803590602001909291908035906020019092919050505061056a565b005b7f5469c6b769315f5668523937f05ca07d4cc87849432bc5f5907f1d90fa73b9f98282604051808381526020018281526020019250505060405180910390a15050565b8082847fb89dabcdb7ff41f1794c0da92f65ece6c19b6b0caeac5407b2a721efe27c080460405160405180910390a4505050565b7fc3f6f1c76bd4e74ee5782052b0b4f8bd5c50b86c3c5a2f52638e03066e50a91b60405160405180910390a1565b817f6709824ebe5f6e620ca3f4b02a3428e8ce2dc97c550816eaeeb3a342b214bd85826040518082815260200191505060405180910390a25050565b7fc804e53d6048af1b3e6a352e246d5f3864fea9d635ace499e023a58c383b3a88816040518082815260200191505060405180910390a150565b807f44a227a31429ab5eb00daf6611c6422f10571619f2267e0e149e9ebe6d2a5d0560405160405180910390a250565b7f28d45631a87b2a52a9625f8520fa37ff8c4d926cdf17042e241985da5cb7b850848484846040518085815260200184815260200183815260200182815260200194505050505060405180910390a150505050565b81837fcd5fe5fbc1d27b90036997224cea7aa565e3779622867265081f636b3a5ccb08836040518082815260200191505060405180910390a3505050565b80827f232f09cef3babc26e58d1cc1346c0a8bc626ffe600c9605b5d747783eda484a760405160405180910390a35050565b8183857f812e73dbcf7e267f27ecb1383bfc902a6650b41b6e7d03ac265108c369673d95846040518082815260200191505060405180910390a450505050565b7fd4d143faaf60340ad98e1f2c96fc26f5695834c21b5200edad339ee7e9a372cc83838360405180848152602001838152602001828152602001935050505060405180910390a150505056fea265627a7a72315820954561fde80ab925299e0a9f3356b01f64fb1976dd335ac2ebd9367441e29f0564736f6c63430005110032 diff --git a/itests/contracts/EventMatrix.sol b/itests/contracts/EventMatrix.sol new file mode 100644 index 000000000..bd008e27b --- /dev/null +++ b/itests/contracts/EventMatrix.sol @@ -0,0 +1,51 @@ +pragma solidity ^0.5.0; + +contract EventMatrix { + event EventZeroData(); + event EventOneData(uint a); + event EventTwoData(uint a, uint b); + event EventThreeData(uint a, uint b, uint c); + event EventFourData(uint a, uint b, uint c, uint d); + + event EventOneIndexed(uint indexed a); + event EventTwoIndexed(uint indexed a, uint indexed b); + event EventThreeIndexed(uint indexed a, uint indexed b, uint indexed c); + + event EventOneIndexedWithData(uint indexed a, uint b); + event EventTwoIndexedWithData(uint indexed a, uint indexed b, uint c); + event EventThreeIndexedWithData(uint indexed a, uint indexed b, uint indexed c, uint d); + + function logEventZeroData() public { + emit EventZeroData(); + } + function logEventOneData(uint a) public { + emit EventOneData(a); + } + function logEventTwoData(uint a, uint b) public { + emit EventTwoData(a,b); + } + function logEventThreeData(uint a, uint b, uint c) public { + emit EventThreeData(a,b,c); + } + function logEventFourData(uint a, uint b, uint c, uint d) public { + emit EventFourData(a,b,c,d); + } + function logEventOneIndexed(uint a) public { + emit EventOneIndexed(a); + } + function logEventTwoIndexed(uint a, uint b) public { + emit EventTwoIndexed(a,b); + } + function logEventThreeIndexed(uint a, uint b, uint c) public { + emit EventThreeIndexed(a,b,c); + } + function logEventOneIndexedWithData(uint a, uint b) public { + emit EventOneIndexedWithData(a,b); + } + function logEventTwoIndexedWithData(uint a, uint b, uint c) public { + emit EventTwoIndexedWithData(a,b,c); + } + function logEventThreeIndexedWithData(uint a, uint b, uint c, uint d) public { + emit EventThreeIndexedWithData(a,b,c,d); + } +} diff --git a/itests/contracts/events.asm b/itests/contracts/events.asm new file mode 100644 index 000000000..ab96fcedd --- /dev/null +++ b/itests/contracts/events.asm @@ -0,0 +1,47 @@ +# https://github.com/filecoin-project/builtin-actors/blob/b1ba61053de2ceaddd5116e87823d20a8f5e38d7/actors/evm/tests/events.rs +# method dispatch: +# - 0x00000000 -> log_zero_data +# - 0x00000001 -> log_zero_nodata +# - 0x00000002 -> log_four_data +%dispatch_begin() +%dispatch(0x00, log_zero_data) +%dispatch(0x01, log_zero_nodata) +%dispatch(0x02, log_four_data) +%dispatch_end() +#### log a zero topic event with data +log_zero_data: +jumpdest +push8 0x1122334455667788 +push1 0x00 +mstore +push1 0x08 +push1 0x18 ## index 24 into memory as mstore writes a full word +log0 +push1 0x00 +push1 0x00 +return +#### log a zero topic event with no data +log_zero_nodata: +jumpdest +push1 0x00 +push1 0x00 +log0 +push1 0x00 +push1 0x00 +return +#### log a four topic event with data +log_four_data: +jumpdest +push8 0x1122334455667788 +push1 0x00 +mstore +push4 0x4444 +push3 0x3333 +push2 0x2222 +push2 0x1111 +push1 0x08 +push1 0x18 ## index 24 into memory as mstore writes a full word +log4 +push1 0x00 +push1 0x00 +return diff --git a/itests/eth_filter_test.go b/itests/eth_filter_test.go index bee9a8c38..c67e4d798 100644 --- a/itests/eth_filter_test.go +++ b/itests/eth_filter_test.go @@ -2,9 +2,12 @@ package itests import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "encoding/json" + "fmt" "os" "path/filepath" "strconv" @@ -12,10 +15,14 @@ import ( "testing" "time" + "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/crypto/sha3" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/api" @@ -25,6 +32,53 @@ import ( "github.com/filecoin-project/lotus/itests/kit" ) +// SolidityContractDef holds information about one of the test contracts +type SolidityContractDef struct { + Filename string // filename of the hex of the contract, e.g. contracts/EventMatrix.hex + Fn map[string][]byte // mapping of function names to 32-bit selector + Ev map[string][]byte // mapping of event names to 256-bit signature hashes +} + +var EventMatrixContract = SolidityContractDef{ + Filename: "contracts/EventMatrix.hex", + Fn: map[string][]byte{ + "logEventZeroData": ethFunctionHash("logEventZeroData()"), + "logEventOneData": ethFunctionHash("logEventOneData(uint256)"), + "logEventTwoData": ethFunctionHash("logEventTwoData(uint256,uint256)"), + "logEventThreeData": ethFunctionHash("logEventThreeData(uint256,uint256,uint256)"), + "logEventFourData": ethFunctionHash("logEventFourData(uint256,uint256,uint256,uint256)"), + "logEventOneIndexed": ethFunctionHash("logEventOneIndexed(uint256)"), + "logEventTwoIndexed": ethFunctionHash("logEventTwoIndexed(uint256,uint256)"), + "logEventThreeIndexed": ethFunctionHash("logEventThreeIndexed(uint256,uint256,uint256)"), + "logEventOneIndexedWithData": ethFunctionHash("logEventOneIndexedWithData(uint256,uint256)"), + "logEventTwoIndexedWithData": ethFunctionHash("logEventTwoIndexedWithData(uint256,uint256,uint256)"), + "logEventThreeIndexedWithData": ethFunctionHash("logEventThreeIndexedWithData(uint256,uint256,uint256,uint256)"), + }, + Ev: map[string][]byte{ + "EventZeroData": ethTopicHash("EventZeroData()"), + "EventOneData": ethTopicHash("EventOneData(uint256)"), + "EventTwoData": ethTopicHash("EventTwoData(uint256,uint256)"), + "EventThreeData": ethTopicHash("EventThreeData(uint256,uint256,uint256)"), + "EventFourData": ethTopicHash("EventFourData(uint256,uint256,uint256,uint256)"), + "EventOneIndexed": ethTopicHash("EventOneIndexed(uint256)"), + "EventTwoIndexed": ethTopicHash("EventTwoIndexed(uint256,uint256)"), + "EventThreeIndexed": ethTopicHash("EventThreeIndexed(uint256,uint256,uint256)"), + "EventOneIndexedWithData": ethTopicHash("EventOneIndexedWithData(uint256,uint256)"), + "EventTwoIndexedWithData": ethTopicHash("EventTwoIndexedWithData(uint256,uint256,uint256)"), + "EventThreeIndexedWithData": ethTopicHash("EventThreeIndexedWithData(uint256,uint256,uint256,uint256)"), + }, +} + +var EventsContract = SolidityContractDef{ + Filename: "contracts/events.bin", + Fn: map[string][]byte{ + "log_zero_data": {0x00, 0x00, 0x00, 0x00}, + "log_zero_nodata": {0x00, 0x00, 0x00, 0x01}, + "log_four_data": {0x00, 0x00, 0x00, 0x02}, + }, + Ev: map[string][]byte{}, +} + func TestEthNewPendingTransactionFilter(t *testing.T) { ctx := context.Background() @@ -109,7 +163,10 @@ func TestEthNewPendingTransactionFilter(t *testing.T) { require.NoError(t, err) // expect to have seen iteration number of mpool messages - require.Equal(t, iterations, len(res.Results)) + require.Equal(t, iterations, len(res.Results), "expected %d tipsets to have been executed", iterations) + + require.Equal(t, len(res.Results), len(expected), "expected number of filter results to equal number of messages") + for _, txid := range res.Results { expected[txid.(string)] = true } @@ -147,6 +204,7 @@ func TestEthNewBlockFilter(t *testing.T) { each := big.Div(toSend, big.NewInt(iterations)) waitAllCh := make(chan struct{}) + tipsetChan := make(chan *types.TipSet, iterations) go func() { headChangeCh, err := client.ChainNotify(ctx) require.NoError(t, err) @@ -159,8 +217,11 @@ func TestEthNewBlockFilter(t *testing.T) { for _, change := range headChanges { if change.Type == store.HCApply || change.Type == store.HCRevert { count++ + tipsetChan <- change.Val if count == iterations { - waitAllCh <- struct{}{} + close(tipsetChan) + close(waitAllCh) + return } } } @@ -168,7 +229,6 @@ func TestEthNewBlockFilter(t *testing.T) { } }() - // var sms []*types.SignedMessage for i := 0; i < iterations; i++ { msg := &types.Message{ From: client.DefaultKey.Address, @@ -179,9 +239,6 @@ func TestEthNewBlockFilter(t *testing.T) { sm, err := client.MpoolPushMessage(ctx, msg, nil) require.NoError(t, err) require.EqualValues(t, i, sm.Message.Nonce) - - // FIXME this was here and unused. Use or remove. - // sms = append(sms, sm) } select { @@ -190,12 +247,31 @@ func TestEthNewBlockFilter(t *testing.T) { t.Errorf("timeout to wait for pack messages") } + expected := make(map[string]bool) + for ts := range tipsetChan { + c, err := ts.Key().Cid() + require.NoError(t, err) + hash, err := ethtypes.EthHashFromCid(c) + require.NoError(t, err) + expected[hash.String()] = false + } + // collect filter results res, err := client.EthGetFilterChanges(ctx, filterID) require.NoError(t, err) // expect to have seen iteration number of tipsets - require.Equal(t, iterations, len(res.Results)) + require.Equal(t, iterations, len(res.Results), "expected %d tipsets to have been executed", iterations) + + require.Equal(t, len(res.Results), len(expected), "expected number of filter results to equal number of tipsets") + + for _, blockhash := range res.Results { + expected[blockhash.(string)] = true + } + + for _, found := range expected { + require.True(t, found, "expected all tipsets to be present in filter results") + } } func TestEthNewFilterCatchAll(t *testing.T) { @@ -311,11 +387,11 @@ func TestEthNewFilterCatchAll(t *testing.T) { // expect to have seen iteration number of events require.Equal(iterations, len(res.Results)) - topic1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x11})) - topic2 := ethtypes.EthBytes(leftpad32([]byte{0x22, 0x22})) - topic3 := ethtypes.EthBytes(leftpad32([]byte{0x33, 0x33})) - topic4 := ethtypes.EthBytes(leftpad32([]byte{0x44, 0x44})) - data1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88})) + topic1 := paddedEthBytes([]byte{0x11, 0x11}) + topic2 := paddedEthBytes([]byte{0x22, 0x22}) + topic3 := paddedEthBytes([]byte{0x33, 0x33}) + topic4 := paddedEthBytes([]byte{0x44, 0x44}) + data1 := paddedEthBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}) for _, r := range res.Results { // since response is a union and Go doesn't support them well, go-jsonrpc won't give us typed results @@ -350,6 +426,50 @@ func TestEthNewFilterCatchAll(t *testing.T) { } } +func parseEthLogsFromSubscriptionResponses(subResponses []ethtypes.EthSubscriptionResponse) ([]*ethtypes.EthLog, error) { + elogs := make([]*ethtypes.EthLog, 0, len(subResponses)) + for i := range subResponses { + rlist, ok := subResponses[i].Result.([]interface{}) + if !ok { + return nil, xerrors.Errorf("expected subscription result to be []interface{}, but was %T", subResponses[i].Result) + } + + for _, r := range rlist { + rmap, ok := r.(map[string]interface{}) + if !ok { + return nil, xerrors.Errorf("expected subscription result entry to be map[string]interface{}, but was %T", r) + } + + elog, err := ParseEthLog(rmap) + if err != nil { + return nil, err + } + elogs = append(elogs, elog) + } + } + + return elogs, nil +} + +func parseEthLogsFromFilterResult(res *ethtypes.EthFilterResult) ([]*ethtypes.EthLog, error) { + elogs := make([]*ethtypes.EthLog, 0, len(res.Results)) + + for _, r := range res.Results { + rmap, ok := r.(map[string]interface{}) + if !ok { + return nil, xerrors.Errorf("expected filter result entry to be map[string]interface{}, but was %T", r) + } + + elog, err := ParseEthLog(rmap) + if err != nil { + return nil, err + } + elogs = append(elogs, elog) + } + + return elogs, nil +} + func ParseEthLog(in map[string]interface{}) (*ethtypes.EthLog, error) { el := ðtypes.EthLog{} @@ -463,109 +583,8 @@ func ParseEthLog(in map[string]interface{}) (*ethtypes.EthLog, error) { return el, err } -type msgInTipset struct { - msg api.Message - ts *types.TipSet - reverted bool -} - -func invokeContractAndWaitUntilAllOnChain(t *testing.T, client *kit.TestFullNode, iterations int) (ethtypes.EthAddress, map[ethtypes.EthHash]msgInTipset) { - require := require.New(t) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - blockTime := 100 * time.Millisecond - - // install contract - contractHex, err := os.ReadFile("contracts/events.bin") - require.NoError(err) - - contract, err := hex.DecodeString(string(contractHex)) - require.NoError(err) - - fromAddr, err := client.WalletDefaultAddress(ctx) - require.NoError(err) - - result := client.EVM().DeployContract(ctx, fromAddr, contract) - - idAddr, err := address.NewIDAddress(result.ActorID) - require.NoError(err) - t.Logf("actor ID address is %s", idAddr) - - msgChan := make(chan msgInTipset, iterations) - - waitAllCh := make(chan struct{}) - go func() { - headChangeCh, err := client.ChainNotify(ctx) - require.NoError(err) - <-headChangeCh // skip hccurrent - - count := 0 - for { - select { - case headChanges := <-headChangeCh: - for _, change := range headChanges { - if change.Type == store.HCApply || change.Type == store.HCRevert { - msgs, err := client.ChainGetMessagesInTipset(ctx, change.Val.Key()) - require.NoError(err) - - count += len(msgs) - for _, m := range msgs { - select { - case msgChan <- msgInTipset{msg: m, ts: change.Val, reverted: change.Type == store.HCRevert}: - default: - } - } - - if count == iterations { - close(msgChan) - close(waitAllCh) - return - } - } - } - } - } - }() - - time.Sleep(blockTime * 6) - - for i := 0; i < iterations; i++ { - // log a four topic event with data - ret := client.EVM().InvokeSolidity(ctx, fromAddr, idAddr, []byte{0x00, 0x00, 0x00, 0x02}, nil) - require.True(ret.Receipt.ExitCode.IsSuccess(), "contract execution failed") - } - - select { - case <-waitAllCh: - case <-time.After(time.Minute): - t.Errorf("timeout to wait for pack messages") - } - - received := make(map[ethtypes.EthHash]msgInTipset) - for m := range msgChan { - eh, err := ethtypes.EthHashFromCid(m.msg.Cid) - require.NoError(err) - received[eh] = m - } - require.Equal(iterations, len(received), "all messages on chain") - - head, err := client.ChainHead(ctx) - require.NoError(err) - - actor, err := client.StateGetActor(ctx, idAddr, head.Key()) - require.NoError(err) - require.NotNil(actor.Address) - ethContractAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address) - require.NoError(err) - - return ethContractAddr, received -} - func TestEthGetLogsAll(t *testing.T) { require := require.New(t) - kit.QuietMiningLogs() blockTime := 100 * time.Millisecond @@ -574,56 +593,38 @@ func TestEthGetLogsAll(t *testing.T) { client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.HistoricFilterAPI(dbpath)) ens.InterconnectAll().BeginMining(blockTime) - ethContractAddr, received := invokeContractAndWaitUntilAllOnChain(t, client, 10) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - topic1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x11})) - topic2 := ethtypes.EthBytes(leftpad32([]byte{0x22, 0x22})) - topic3 := ethtypes.EthBytes(leftpad32([]byte{0x33, 0x33})) - topic4 := ethtypes.EthBytes(leftpad32([]byte{0x44, 0x44})) - data1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88})) + invocations := 1 + ethContractAddr, received := invokeLogFourData(t, client, invocations) - pstring := func(s string) *string { return &s } + // Build filter spec + spec := EthFilterBuilder(). + FromBlockEpoch(0). + Topic1OneOf(paddedEthHash([]byte{0x11, 0x11})). + Filter() - // get all logs - res, err := client.EthGetLogs(context.Background(), ðtypes.EthFilterSpec{ - FromBlock: pstring("0x0"), - }) + expected := []ExpectedEthLog{ + { + Address: ethContractAddr, + Topics: []ethtypes.EthBytes{ + paddedEthBytes([]byte{0x11, 0x11}), + paddedEthBytes([]byte{0x22, 0x22}), + paddedEthBytes([]byte{0x33, 0x33}), + paddedEthBytes([]byte{0x44, 0x44}), + }, + Data: paddedEthBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}), + }, + } + + // Use filter + res, err := client.EthGetLogs(ctx, spec) require.NoError(err) - // expect to have all messages sent - require.Equal(len(received), len(res.Results)) - - for _, r := range res.Results { - // since response is a union and Go doesn't support them well, go-jsonrpc won't give us typed results - rc, ok := r.(map[string]interface{}) - require.True(ok, "result type") - - elog, err := ParseEthLog(rc) - require.NoError(err) - - require.Equal(ethContractAddr, elog.Address, "event address") - require.Equal(ethtypes.EthUint64(0), elog.TransactionIndex, "transaction index") // only one message per tipset - - msg, exists := received[elog.TransactionHash] - require.True(exists, "message seen on chain") - - tsCid, err := msg.ts.Key().Cid() - require.NoError(err) - - tsCidHash, err := ethtypes.EthHashFromCid(tsCid) - require.NoError(err) - - require.Equal(tsCidHash, elog.BlockHash, "block hash") - - require.Equal(4, len(elog.Topics), "number of topics") - require.Equal(topic1, elog.Topics[0], "topic1") - require.Equal(topic2, elog.Topics[1], "topic2") - require.Equal(topic3, elog.Topics[2], "topic3") - require.Equal(topic4, elog.Topics[3], "topic4") - - require.Equal(data1, elog.Data, "data1") - - } + elogs, err := parseEthLogsFromFilterResult(res) + require.NoError(err) + AssertEthLogs(t, elogs, expected, received) } func TestEthGetLogsByTopic(t *testing.T) { @@ -638,14 +639,7 @@ func TestEthGetLogsByTopic(t *testing.T) { ens.InterconnectAll().BeginMining(blockTime) invocations := 1 - - ethContractAddr, received := invokeContractAndWaitUntilAllOnChain(t, client, invocations) - - topic1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x11})) - topic2 := ethtypes.EthBytes(leftpad32([]byte{0x22, 0x22})) - topic3 := ethtypes.EthBytes(leftpad32([]byte{0x33, 0x33})) - topic4 := ethtypes.EthBytes(leftpad32([]byte{0x44, 0x44})) - data1 := ethtypes.EthBytes(leftpad32([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88})) + ethContractAddr, received := invokeLogFourData(t, client, invocations) // find log by known topic1 var spec ethtypes.EthFilterSpec @@ -655,39 +649,21 @@ func TestEthGetLogsByTopic(t *testing.T) { res, err := client.EthGetLogs(context.Background(), &spec) require.NoError(err) - require.Equal(invocations, len(res.Results)) - - for _, r := range res.Results { - // since response is a union and Go doesn't support them well, go-jsonrpc won't give us typed results - rc, ok := r.(map[string]interface{}) - require.True(ok, "result type") - - elog, err := ParseEthLog(rc) - require.NoError(err) - - require.Equal(ethContractAddr, elog.Address, "event address") - require.Equal(ethtypes.EthUint64(0), elog.TransactionIndex, "transaction index") // only one message per tipset - - msg, exists := received[elog.TransactionHash] - require.True(exists, "message seen on chain") - - tsCid, err := msg.ts.Key().Cid() - require.NoError(err) - - tsCidHash, err := ethtypes.EthHashFromCid(tsCid) - require.NoError(err) - - require.Equal(tsCidHash, elog.BlockHash, "block hash") - - require.Equal(4, len(elog.Topics), "number of topics") - require.Equal(topic1, elog.Topics[0], "topic1") - require.Equal(topic2, elog.Topics[1], "topic2") - require.Equal(topic3, elog.Topics[2], "topic3") - require.Equal(topic4, elog.Topics[3], "topic4") - - require.Equal(data1, elog.Data, "data1") - + expected := []ExpectedEthLog{ + { + Address: ethContractAddr, + Topics: []ethtypes.EthBytes{ + paddedEthBytes([]byte{0x11, 0x11}), + paddedEthBytes([]byte{0x22, 0x22}), + paddedEthBytes([]byte{0x33, 0x33}), + paddedEthBytes([]byte{0x44, 0x44}), + }, + Data: paddedEthBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}), + }, } + elogs, err := parseEthLogsFromFilterResult(res) + require.NoError(err) + AssertEthLogs(t, elogs, expected, received) } func TestEthSubscribeLogs(t *testing.T) { @@ -730,23 +706,587 @@ func TestEthSubscribeLogs(t *testing.T) { }() const iterations = 10 + ethContractAddr, messages := invokeLogFourData(t, client, iterations) - type msgInTipset struct { - msg api.Message - ts *types.TipSet + expected := make([]ExpectedEthLog, iterations) + for i := range expected { + expected[i] = ExpectedEthLog{ + Address: ethContractAddr, + Topics: []ethtypes.EthBytes{ + paddedEthBytes([]byte{0x11, 0x11}), + paddedEthBytes([]byte{0x22, 0x22}), + paddedEthBytes([]byte{0x33, 0x33}), + paddedEthBytes([]byte{0x44, 0x44}), + }, + Data: paddedEthBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}), + } } - msgChan := make(chan msgInTipset, iterations) + elogs, err := parseEthLogsFromSubscriptionResponses(subResponses) + require.NoError(err) + AssertEthLogs(t, elogs, expected, messages) +} + +func TestEthGetLogs(t *testing.T) { + require := require.New(t) + kit.QuietMiningLogs() + + blockTime := 100 * time.Millisecond + dbpath := filepath.Join(t.TempDir(), "actorevents.db") + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.HistoricFilterAPI(dbpath)) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + contract1, contract2, messages := invokeEventMatrix(ctx, t, client) + + testCases := []struct { + name string + spec *ethtypes.EthFilterSpec + expected []ExpectedEthLog + }{ + { + name: "find all EventZeroData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventZeroData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventZeroData"], + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventZeroData"], + }, + Data: nil, + }, + }, + }, + { + name: "find all EventOneData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventOneData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneData"], + }, + Data: packUint64Values(23), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneData"], + }, + Data: packUint64Values(44), + }, + }, + }, + { + name: "find all EventTwoData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoData"], + }, + Data: packUint64Values(555, 666), + }, + }, + }, + { + name: "find all EventThreeData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventThreeData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeData"], + }, + Data: packUint64Values(1, 2, 3), + }, + }, + }, + { + name: "find all EventOneIndexed events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventOneIndexed"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexed"], + paddedUint64(44), + }, + Data: nil, + }, + }, + }, + { + name: "find all EventTwoIndexed events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoIndexed"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(44), + paddedUint64(19), + }, + Data: nil, + }, + }, + }, + { + name: "find all EventThreeIndexed events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventThreeIndexed"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexed"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: nil, + }, + }, + }, + { + name: "find all EventOneIndexedWithData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventOneIndexedWithData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(44), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(46), + }, + Data: paddedUint64(12), + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(50), + }, + Data: paddedUint64(9), + }, + }, + }, + { + name: "find all EventTwoIndexedWithData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoIndexedWithData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(44), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(14), + }, + Data: paddedUint64(19), + }, + }, + }, + { + name: "find all EventThreeIndexedWithData events", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventThreeIndexedWithData"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexedWithData"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: paddedUint64(12), + }, + }, + }, + + { + name: "find all events from contract2", + spec: EthFilterBuilder().FromBlockEpoch(0).AddressOneOf(contract2).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventZeroData"], + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexed"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(44), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(50), + }, + Data: paddedUint64(9), + }, + }, + }, + + { + name: "find all events with topic2 of 44", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic2OneOf(paddedEthHash(paddedUint64(44))).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexed"], + paddedUint64(44), + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(44), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexed"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(44), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(44), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexedWithData"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: paddedUint64(12), + }, + }, + }, + + { + name: "find all events with topic2 of 44 from contract2", + spec: EthFilterBuilder().FromBlockEpoch(0).AddressOneOf(contract2).Topic2OneOf(paddedEthHash(paddedUint64(44))).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventThreeIndexed"], + paddedUint64(44), + paddedUint64(27), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(44), + paddedUint64(19), + }, + Data: nil, + }, + }, + }, + + { + name: "find all events with topic2 of 46", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic2OneOf(paddedEthHash(paddedUint64(46))).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(46), + }, + Data: paddedUint64(12), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(14), + }, + Data: paddedUint64(19), + }, + }, + }, + { + name: "find all events with topic2 of 50", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic2OneOf(paddedEthHash(paddedUint64(50))).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(50), + }, + Data: paddedUint64(9), + }, + }, + }, + { + name: "find all events with topic2 of 46 or 50", + spec: EthFilterBuilder().FromBlockEpoch(0).Topic2OneOf(paddedEthHash(paddedUint64(46)), paddedEthHash(paddedUint64(50))).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(46), + }, + Data: paddedUint64(12), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(14), + }, + Data: paddedUint64(19), + }, + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventOneIndexedWithData"], + paddedUint64(50), + }, + Data: paddedUint64(9), + }, + }, + }, + + { + name: "find all events with topic1 of EventTwoIndexedWithData and topic3 of 27", + spec: EthFilterBuilder(). + FromBlockEpoch(0). + Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoIndexedWithData"])). + Topic3OneOf(paddedEthHash(paddedUint64(27))). + Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(44), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(46), + paddedUint64(27), + }, + Data: paddedUint64(19), + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc // appease the lint despot + t.Run(tc.name, func(t *testing.T) { + res, err := client.EthGetLogs(ctx, tc.spec) + require.NoError(err) + + elogs, err := parseEthLogsFromFilterResult(res) + require.NoError(err) + AssertEthLogs(t, elogs, tc.expected, messages) + }) + } +} + +// ------------------------------------------------------------------------------- +// end of tests +// ------------------------------------------------------------------------------- + +type msgInTipset struct { + msg api.Message + events []types.Event // events extracted from receipt + ts *types.TipSet + reverted bool +} + +func installContract(ctx context.Context, t *testing.T, client *kit.TestFullNode, filename string) (address.Address, address.Address) { + sender, err := client.WalletDefaultAddress(ctx) + require.NoError(t, err) + + result := client.EVM().DeployContractFile(ctx, sender, filename) + + actorAddr, err := address.NewIDAddress(result.ActorID) + require.NoError(t, err) + + return sender, actorAddr +} + +func getContractEthAddress(ctx context.Context, t *testing.T, client *kit.TestFullNode, idAddr address.Address) ethtypes.EthAddress { + head, err := client.ChainHead(ctx) + require.NoError(t, err) + + actor, err := client.StateGetActor(ctx, idAddr, head.Key()) + require.NoError(t, err) + require.NotNil(t, actor.Address) + ethContractAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address) + require.NoError(t, err) + return ethContractAddr +} + +type Invocation struct { + Sender address.Address + Target address.Address + Selector []byte // function selector + Data []byte + MinHeight abi.ChainEpoch // minimum chain height that must be reached before invoking +} + +func invokeAndWaitUntilAllOnChain(t *testing.T, client *kit.TestFullNode, invocations []Invocation) map[ethtypes.EthHash]msgInTipset { + require := require.New(t) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + msgChan := make(chan msgInTipset, len(invocations)) waitAllCh := make(chan struct{}) + waitForFirstHeadChange := make(chan struct{}) go func() { headChangeCh, err := client.ChainNotify(ctx) require.NoError(err) - <-headChangeCh // skip hccurrent + select { + case <-ctx.Done(): + return + case <-headChangeCh: // skip hccurrent + } + + close(waitForFirstHeadChange) + + defer func() { + close(msgChan) + close(waitAllCh) + }() count := 0 for { select { + case <-ctx.Done(): + return case headChanges := <-headChangeCh: for _, change := range headChanges { if change.Type == store.HCApply || change.Type == store.HCRevert { @@ -756,14 +1296,12 @@ func TestEthSubscribeLogs(t *testing.T) { count += len(msgs) for _, m := range msgs { select { - case msgChan <- msgInTipset{msg: m, ts: change.Val}: + case msgChan <- msgInTipset{msg: m, ts: change.Val, reverted: change.Type == store.HCRevert}: default: } } - if count == iterations { - close(msgChan) - close(waitAllCh) + if count == len(invocations) { return } } @@ -772,39 +1310,378 @@ func TestEthSubscribeLogs(t *testing.T) { } }() - time.Sleep(blockTime * 6) + select { + case <-waitForFirstHeadChange: + case <-ctx.Done(): + t.Fatalf("timeout waiting for first head change") + } - for i := 0; i < iterations; i++ { - // log a four topic event with data - ret := client.EVM().InvokeSolidity(ctx, fromAddr, idAddr, []byte{0x00, 0x00, 0x00, 0x02}, nil) + events := map[cid.Cid][]types.Event{} + for _, inv := range invocations { + if inv.MinHeight > 0 { + for { + ts, err := client.ChainHead(ctx) + require.NoError(err) + if ts.Height() >= inv.MinHeight { + break + } + select { + case <-ctx.Done(): + t.Fatalf("context cancelled") + case <-time.After(100 * time.Millisecond): + } + } + } + ret := client.EVM().InvokeSolidity(ctx, inv.Sender, inv.Target, inv.Selector, inv.Data) require.True(ret.Receipt.ExitCode.IsSuccess(), "contract execution failed") + + require.NotNil(t, ret.Receipt.EventsRoot, "no event root on receipt") + + evs := client.EVM().LoadEvents(ctx, *ret.Receipt.EventsRoot) + events[ret.Message] = evs } select { case <-waitAllCh: - case <-time.After(time.Minute): - t.Errorf("timeout to wait for pack messages") - } - - if len(subResponses) > 0 { - ok, err := client.EthUnsubscribe(ctx, subResponses[0].SubscriptionID) - require.NoError(err) - require.True(ok, "unsubscribed") + case <-ctx.Done(): + t.Fatalf("timeout waiting to pack messages") } received := make(map[ethtypes.EthHash]msgInTipset) for m := range msgChan { + evs, ok := events[m.msg.Cid] + require.True(ok) + m.events = evs + eh, err := ethtypes.EthHashFromCid(m.msg.Cid) require.NoError(err) received[eh] = m } - require.Equal(iterations, len(received), "all messages on chain") + require.Equal(len(invocations), len(received), "all messages on chain") - // expect to have seen all logs - require.Equal(len(received), len(subResponses)) + return received } -func leftpad32(orig []byte) []byte { +func invokeLogFourData(t *testing.T, client *kit.TestFullNode, iterations int) (ethtypes.EthAddress, map[ethtypes.EthHash]msgInTipset) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + fromAddr, idAddr := installContract(ctx, t, client, EventsContract.Filename) + + invocations := make([]Invocation, iterations) + for i := range invocations { + invocations[i] = Invocation{ + Sender: fromAddr, + Target: idAddr, + Selector: EventsContract.Fn["log_four_data"], + Data: nil, + } + } + + messages := invokeAndWaitUntilAllOnChain(t, client, invocations) + + ethAddr := getContractEthAddress(ctx, t, client, idAddr) + + return ethAddr, messages +} + +func invokeEventMatrix(ctx context.Context, t *testing.T, client *kit.TestFullNode) (ethtypes.EthAddress, ethtypes.EthAddress, map[ethtypes.EthHash]msgInTipset) { + sender1, contract1 := installContract(ctx, t, client, EventMatrixContract.Filename) + sender2, contract2 := installContract(ctx, t, client, EventMatrixContract.Filename) + + invocations := []Invocation{ + // log EventZeroData() + // topic1: hash(EventZeroData) + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventZeroData"], + Data: nil, + }, + + // log EventOneData(23) + // topic1: hash(EventOneData) + // data: 23 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventOneData"], + Data: packUint64Values(23), + }, + + // log EventOneIndexed(44) + // topic1: hash(EventOneIndexed) + // topic2: 44 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventOneIndexed"], + Data: packUint64Values(44), + }, + + // log EventTwoIndexed(44,19) from contract2 + // topic1: hash(EventTwoIndexed) + // topic2: 44 + // topic3: 19 + { + Sender: sender1, + Target: contract2, + Selector: EventMatrixContract.Fn["logEventTwoIndexed"], + Data: packUint64Values(44, 19), + }, + + // log EventOneData(44) + // topic1: hash(EventOneData) + // data: 44 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventOneData"], + Data: packUint64Values(44), + }, + + // log EventTwoData(555,666) + // topic1: hash(EventTwoData) + // data: 555,666 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventTwoData"], + Data: packUint64Values(555, 666), + }, + + // log EventZeroData() from contract2 + // topic1: hash(EventZeroData) + { + Sender: sender2, + Target: contract2, + Selector: EventMatrixContract.Fn["logEventZeroData"], + Data: nil, + }, + + // log EventThreeData(1,2,3) + // topic1: hash(EventTwoData) + // data: 1,2,3 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventThreeData"], + Data: packUint64Values(1, 2, 3), + }, + + // log EventThreeIndexed(44,27,19) from contract2 + // topic1: hash(EventThreeIndexed) + // topic2: 44 + // topic3: 27 + // topic4: 19 + { + Sender: sender1, + Target: contract2, + Selector: EventMatrixContract.Fn["logEventThreeIndexed"], + Data: packUint64Values(44, 27, 19), + }, + + // log EventOneIndexedWithData(44,19) + // topic1: hash(EventOneIndexedWithData) + // topic2: 44 + // data: 19 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventOneIndexedWithData"], + Data: packUint64Values(44, 19), + }, + + // log EventOneIndexedWithData(46,12) + // topic1: hash(EventOneIndexedWithData) + // topic2: 46 + // data: 12 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventOneIndexedWithData"], + Data: packUint64Values(46, 12), + }, + + // log EventTwoIndexedWithData(44,27,19) + // topic1: hash(EventTwoIndexedWithData) + // topic2: 44 + // topic3: 27 + // data: 19 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventTwoIndexedWithData"], + Data: packUint64Values(44, 27, 19), + }, + + // log EventThreeIndexedWithData(44,27,19,12) + // topic1: hash(EventThreeIndexedWithData) + // topic2: 44 + // topic3: 27 + // topic4: 19 + // data: 12 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventThreeIndexedWithData"], + Data: packUint64Values(44, 27, 19, 12), + }, + + // log EventOneIndexedWithData(50,9) + // topic1: hash(EventOneIndexedWithData) + // topic2: 50 + // data: 9 + { + Sender: sender2, + Target: contract2, + Selector: EventMatrixContract.Fn["logEventOneIndexedWithData"], + Data: packUint64Values(50, 9), + }, + + // log EventTwoIndexedWithData(46,27,19) + // topic1: hash(EventTwoIndexedWithData) + // topic2: 46 + // topic3: 27 + // data: 19 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventTwoIndexedWithData"], + Data: packUint64Values(46, 27, 19), + }, + + // log EventTwoIndexedWithData(46,14,19) + // topic1: hash(EventTwoIndexedWithData) + // topic2: 46 + // topic3: 14 + // data: 19 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventTwoIndexedWithData"], + Data: packUint64Values(46, 14, 19), + }, + } + + messages := invokeAndWaitUntilAllOnChain(t, client, invocations) + ethAddr1 := getContractEthAddress(ctx, t, client, contract1) + ethAddr2 := getContractEthAddress(ctx, t, client, contract2) + return ethAddr1, ethAddr2, messages +} + +type ExpectedEthLog struct { + // Address is the address of the actor that produced the event log. + Address ethtypes.EthAddress `json:"address"` + + // List of topics associated with the event log. + Topics []ethtypes.EthBytes `json:"topics"` + + // Data is the value of the event log, excluding topics + Data ethtypes.EthBytes `json:"data"` +} + +func AssertEthLogs(t *testing.T, actual []*ethtypes.EthLog, expected []ExpectedEthLog, messages map[ethtypes.EthHash]msgInTipset) { + require := require.New(t) + // require.Equal(len(expected), len(actual), "number of results equal to expected") + + formatTopics := func(topics []ethtypes.EthBytes) string { + ss := make([]string, len(topics)) + for i := range topics { + ss[i] = fmt.Sprintf("%d:%x", i, topics[i]) + } + return strings.Join(ss, ",") + } + + expectedMatched := map[int]bool{} + + for _, elog := range actual { + msg, exists := messages[elog.TransactionHash] + require.True(exists, "message seen on chain") + + tsCid, err := msg.ts.Key().Cid() + require.NoError(err) + + tsCidHash, err := ethtypes.EthHashFromCid(tsCid) + require.NoError(err) + + require.Equal(tsCidHash, elog.BlockHash, "block hash matches tipset key") + + // Try and match the received log against an expected log + matched := false + LoopExpected: + for i, want := range expected { + // each expected log must match only once + if expectedMatched[i] { + continue + } + + if elog.Address != want.Address { + continue + } + + if len(elog.Topics) != len(want.Topics) { + continue + } + + for j := range elog.Topics { + if !bytes.Equal(elog.Topics[j], want.Topics[j]) { + continue LoopExpected + } + } + + if !bytes.Equal(elog.Data, want.Data) { + continue + } + + expectedMatched[i] = true + matched = true + break + } + + if !matched { + var buf strings.Builder + buf.WriteString("found unexpected log:\n") + buf.WriteString(fmt.Sprintf(" address: %s\n", elog.Address)) + buf.WriteString(fmt.Sprintf(" topics: %s\n", formatTopics(elog.Topics))) + buf.WriteString(fmt.Sprintf(" data: %x\n", elog.Data)) + buf.WriteString("original events from receipt were:\n") + for i, ev := range msg.events { + buf.WriteString(fmt.Sprintf("event %d\n", i)) + buf.WriteString(fmt.Sprintf(" emitter: %v\n", ev.Emitter)) + for _, en := range ev.Entries { + buf.WriteString(fmt.Sprintf(" %s=%x\n", en.Key, decodeLogBytes(en.Value))) + } + } + + t.Errorf(buf.String()) + } + } + + for i := range expected { + if _, ok := expectedMatched[i]; !ok { + var buf strings.Builder + buf.WriteString(fmt.Sprintf("did not find expected log with index %d:\n", i)) + buf.WriteString(fmt.Sprintf(" address: %s\n", expected[i].Address)) + buf.WriteString(fmt.Sprintf(" topics: %s\n", formatTopics(expected[i].Topics))) + buf.WriteString(fmt.Sprintf(" data: %x\n", expected[i].Data)) + t.Errorf(buf.String()) + } + } +} + +func decodeLogBytes(orig []byte) []byte { + if orig == nil { + return orig + } + decoded, err := cbg.ReadByteArray(bytes.NewReader(orig), uint64(len(orig))) + if err != nil { + return orig + } + return decoded +} + +func paddedEthBytes(orig []byte) ethtypes.EthBytes { needed := 32 - len(orig) if needed <= 0 { return orig @@ -813,3 +1690,112 @@ func leftpad32(orig []byte) []byte { copy(ret[needed:], orig) return ret } + +func paddedUint64(v uint64) ethtypes.EthBytes { + buf := make([]byte, 32) + binary.BigEndian.PutUint64(buf[24:], v) + return buf +} + +func paddedEthHash(orig []byte) ethtypes.EthHash { + if len(orig) > 32 { + panic("exceeds EthHash length") + } + var ret ethtypes.EthHash + needed := 32 - len(orig) + copy(ret[needed:], orig) + return ret +} + +func ethTopicHash(sig string) []byte { + hasher := sha3.NewLegacyKeccak256() + hasher.Write([]byte(sig)) + return hasher.Sum(nil) +} + +func ethFunctionHash(sig string) []byte { + hasher := sha3.NewLegacyKeccak256() + hasher.Write([]byte(sig)) + return hasher.Sum(nil)[:4] +} + +func packUint64Values(vals ...uint64) []byte { + ret := []byte{} + for _, v := range vals { + buf := paddedUint64(v) + ret = append(ret, buf...) + } + return ret +} + +func EthFilterBuilder() *ethFilterBuilder { return ðFilterBuilder{} } + +type ethFilterBuilder struct { + filter ethtypes.EthFilterSpec +} + +func (e *ethFilterBuilder) Filter() *ethtypes.EthFilterSpec { return &e.filter } + +func (e *ethFilterBuilder) FromBlock(v string) *ethFilterBuilder { + e.filter.FromBlock = &v + return e +} + +func (e *ethFilterBuilder) FromBlockEpoch(v uint64) *ethFilterBuilder { + s := ethtypes.EthUint64(v).Hex() + e.filter.FromBlock = &s + return e +} + +func (e *ethFilterBuilder) ToBlock(v string) *ethFilterBuilder { + e.filter.ToBlock = &v + return e +} + +func (e *ethFilterBuilder) ToBlockEpoch(v uint64) *ethFilterBuilder { + s := ethtypes.EthUint64(v).Hex() + e.filter.ToBlock = &s + return e +} + +func (e *ethFilterBuilder) BlockHash(h ethtypes.EthHash) *ethFilterBuilder { + e.filter.BlockHash = &h + return e +} + +func (e *ethFilterBuilder) AddressOneOf(as ...ethtypes.EthAddress) *ethFilterBuilder { + e.filter.Address = as + return e +} + +func (e *ethFilterBuilder) Topic1OneOf(hs ...ethtypes.EthHash) *ethFilterBuilder { + if len(e.filter.Topics) == 0 { + e.filter.Topics = make(ethtypes.EthTopicSpec, 1) + } + e.filter.Topics[0] = hs + return e +} + +func (e *ethFilterBuilder) Topic2OneOf(hs ...ethtypes.EthHash) *ethFilterBuilder { + for len(e.filter.Topics) < 2 { + e.filter.Topics = append(e.filter.Topics, nil) + } + e.filter.Topics[1] = hs + return e +} + +func (e *ethFilterBuilder) Topic3OneOf(hs ...ethtypes.EthHash) *ethFilterBuilder { + for len(e.filter.Topics) < 3 { + e.filter.Topics = append(e.filter.Topics, nil) + } + e.filter.Topics[2] = hs + return e +} + +func (e *ethFilterBuilder) Topic4OneOf(hs ...ethtypes.EthHash) *ethFilterBuilder { + for len(e.filter.Topics) < 4 { + e.filter.Topics = append(e.filter.Topics, nil) + } + e.filter.Topics[3] = hs + return e +} diff --git a/itests/kit/evm.go b/itests/kit/evm.go index 0c12229f5..2b61f6e8d 100644 --- a/itests/kit/evm.go +++ b/itests/kit/evm.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "encoding/binary" + "encoding/hex" "fmt" + "os" "github.com/ipfs/go-cid" "github.com/multiformats/go-varint" @@ -77,6 +79,21 @@ func (e *EVM) DeployContract(ctx context.Context, sender address.Address, byteco return result } +func (e *EVM) DeployContractFile(ctx context.Context, sender address.Address, filename string) eam.CreateReturn { + require := require.New(e.t) + + contractHex, err := os.ReadFile(filename) + require.NoError(err) + + // strip any trailing newlines from the file + contractHex = bytes.TrimRight(contractHex, "\n") + + bytecode, err := hex.DecodeString(string(contractHex)) + require.NoError(err) + + return e.DeployContract(ctx, sender, bytecode) +} + func (e *EVM) InvokeSolidity(ctx context.Context, sender address.Address, target address.Address, selector []byte, inputData []byte) *api.MsgLookup { require := require.New(e.t) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 71770e847..aa7db37ab 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -1161,7 +1161,7 @@ func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*ethtypes.EthFilte var err error for _, entry := range ev.Entries { - value := ethtypes.EthBytes(leftpad32(decodeLogBytes(entry.Value))) + value := ethtypes.EthBytes(leftpad32(entry.Value)) // value has already been cbor-decoded but see https://github.com/filecoin-project/ref-fvm/issues/1345 if entry.Key == ethtypes.EthTopic1 || entry.Key == ethtypes.EthTopic2 || entry.Key == ethtypes.EthTopic3 || entry.Key == ethtypes.EthTopic4 { log.Topics = append(log.Topics, value) } else {