diff --git a/chain/events/filter/event.go b/chain/events/filter/event.go index 779b0c186..7d59ad8bd 100644 --- a/chain/events/filter/event.go +++ b/chain/events/filter/event.go @@ -100,9 +100,18 @@ func (f *EventFilter) CollectEvents(ctx context.Context, te *TipSetEvents, rever continue } + decodedEntries := make([]types.EventEntry, len(ev.Entries)) + for i, entry := range ev.Entries { + decodedEntries[i] = types.EventEntry{ + Flags: entry.Flags, + Key: entry.Key, + Value: decodeLogBytes(entry.Value), + } + } + // event matches filter, so record it cev := &CollectedEvent{ - Entries: ev.Entries, + Entries: decodedEntries, EmitterAddr: addr, EventIdx: evIdx, Reverted: revert, diff --git a/chain/events/filter/index.go b/chain/events/filter/index.go index 45cabaa11..1920a91fe 100644 --- a/chain/events/filter/index.go +++ b/chain/events/filter/index.go @@ -225,7 +225,7 @@ func (ei *EventIndex) CollectEvents(ctx context.Context, te *TipSetEvents, rever // This function swallows errors and returns the original array if it failed // to decode. func decodeLogBytes(orig []byte) []byte { - if orig == nil { + if len(orig) == 0 { return orig } decoded, err := cbg.ReadByteArray(bytes.NewReader(orig), uint64(len(orig))) diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index ea1322d67..eb0e12891 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 a44601b32..aba61f934 100644 --- a/itests/eth_filter_test.go +++ b/itests/eth_filter_test.go @@ -2,19 +2,27 @@ package itests import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "encoding/json" + "fmt" "os" + "sort" "strconv" "strings" "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" @@ -24,10 +32,58 @@ import ( "github.com/filecoin-project/lotus/itests/kit" ) -func TestEthNewPendingTransactionFilter(t *testing.T) { - ctx := context.Background() +// 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 +} - kit.QuietMiningLogs() +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, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + kit.QuietAllLogsExcept("events", "messagepool") client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) ens.InterconnectAll().BeginMining(10 * time.Millisecond) @@ -57,9 +113,15 @@ func TestEthNewPendingTransactionFilter(t *testing.T) { require.NoError(t, err) <-headChangeCh // skip hccurrent + defer func() { + close(waitAllCh) + }() + count := 0 for { select { + case <-ctx.Done(): + return case headChanges := <-headChangeCh: for _, change := range headChanges { if change.Type == store.HCApply { @@ -67,7 +129,7 @@ func TestEthNewPendingTransactionFilter(t *testing.T) { require.NoError(t, err) count += len(msgs) if count == iterations { - waitAllCh <- struct{}{} + return } } } @@ -92,8 +154,8 @@ func TestEthNewPendingTransactionFilter(t *testing.T) { select { case <-waitAllCh: - case <-time.After(time.Minute): - t.Errorf("timeout to wait for pack messages") + case <-ctx.Done(): + t.Errorf("timeout waiting to pack messages") } expected := make(map[string]bool) @@ -108,7 +170,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 } @@ -119,9 +184,10 @@ func TestEthNewPendingTransactionFilter(t *testing.T) { } func TestEthNewBlockFilter(t *testing.T) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - kit.QuietMiningLogs() + kit.QuietAllLogsExcept("events", "messagepool") client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) ens.InterconnectAll().BeginMining(10 * time.Millisecond) @@ -146,20 +212,29 @@ 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) <-headChangeCh // skip hccurrent + defer func() { + close(tipsetChan) + 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 { count++ + tipsetChan <- change.Val if count == iterations { - waitAllCh <- struct{}{} + return } } } @@ -167,7 +242,6 @@ func TestEthNewBlockFilter(t *testing.T) { } }() - // var sms []*types.SignedMessage for i := 0; i < iterations; i++ { msg := &types.Message{ From: client.DefaultKey.Address, @@ -178,15 +252,21 @@ 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 { case <-waitAllCh: - case <-time.After(time.Minute): - t.Errorf("timeout to wait for pack messages") + case <-ctx.Done(): + t.Errorf("timeout waiting to 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 @@ -194,13 +274,23 @@ func TestEthNewBlockFilter(t *testing.T) { 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) { require := require.New(t) - kit.QuietMiningLogs() + kit.QuietAllLogsExcept("events", "messagepool") blockTime := 100 * time.Millisecond client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) @@ -229,24 +319,1033 @@ func TestEthNewFilterCatchAll(t *testing.T) { filterID, err := client.EthNewFilter(ctx, ðtypes.EthFilterSpec{}) require.NoError(err) - const iterations = 10 + const iterations = 3 + ethContractAddr, received := invokeLogFourData(t, client, iterations) - type msgInTipset struct { - msg api.Message - ts *types.TipSet + // collect filter results + res, err := client.EthGetFilterChanges(ctx, filterID) + require.NoError(err) + + // expect to have seen iteration number of events + require.Equal(iterations, len(res.Results)) + + 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}), + }, + { + 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}), + }, + { + 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 := parseEthLogsFromFilterResult(res) + require.NoError(err) + AssertEthLogs(t, elogs, expected, received) +} + +func TestEthGetLogsAll(t *testing.T) { + require := require.New(t) + kit.QuietAllLogsExcept("events", "messagepool") + + blockTime := 100 * time.Millisecond + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + invocations := 1 + ethContractAddr, received := invokeLogFourData(t, client, invocations) + + // Build filter spec + spec := newEthFilterBuilder(). + FromBlockEpoch(0). + Topic1OneOf(paddedEthHash([]byte{0x11, 0x11})). + Filter() + + 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) + + elogs, err := parseEthLogsFromFilterResult(res) + require.NoError(err) + AssertEthLogs(t, elogs, expected, received) +} + +func TestEthGetLogsByTopic(t *testing.T) { + require := require.New(t) + + kit.QuietAllLogsExcept("events", "messagepool") + + blockTime := 100 * time.Millisecond + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + invocations := 1 + ethContractAddr, received := invokeLogFourData(t, client, invocations) + + // find log by known topic1 + var spec ethtypes.EthFilterSpec + err := json.Unmarshal([]byte(`{"fromBlock":"0x0","topics":["0x0000000000000000000000000000000000000000000000000000000000001111"]}`), &spec) + require.NoError(err) + + res, err := client.EthGetLogs(context.Background(), &spec) + require.NoError(err) + + 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) { + require := require.New(t) + + kit.QuietAllLogsExcept("events", "messagepool") + + blockTime := 100 * time.Millisecond + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // 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) + + // install filter + respCh, err := client.EthSubscribe(ctx, "logs", nil) + require.NoError(err) + + subResponses := []ethtypes.EthSubscriptionResponse{} + go func() { + for resp := range respCh { + subResponses = append(subResponses, resp) + } + }() + + const iterations = 10 + ethContractAddr, messages := invokeLogFourData(t, client, iterations) + + 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}), + } + } + + elogs, err := parseEthLogsFromSubscriptionResponses(subResponses) + require.NoError(err) + AssertEthLogs(t, elogs, expected, messages) +} + +func TestEthGetLogs(t *testing.T) { + require := require.New(t) + kit.QuietAllLogsExcept("events", "messagepool") + + blockTime := 100 * time.Millisecond + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Set up the test fixture with a standard list of invocations + contract1, contract2, messages := invokeEventMatrix(ctx, t, client) + + testCases := []struct { + name string + spec *ethtypes.EthFilterSpec + expected []ExpectedEthLog + }{ + { + name: "find all EventZeroData events", + spec: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoIndexed"])).Filter(), + + expected: []ExpectedEthLog{ + { + Address: contract2, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(44), + paddedUint64(19), + }, + Data: nil, + }, + { + Address: contract1, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexed"], + paddedUint64(40), + paddedUint64(20), + }, + Data: nil, + }, + }, + }, + { + name: "find all EventThreeIndexed events", + spec: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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 EventOneIndexedWithData events from contract1 or contract2", + spec: newEthFilterBuilder(). + FromBlockEpoch(0). + AddressOneOf(contract1, contract2). + 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 events with topic2 of 46", + spec: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder().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: newEthFilterBuilder(). + 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), + }, + }, + }, + + { + name: "find all events with topic1 of EventTwoIndexedWithData or EventOneIndexed and topic2 of 44", + spec: newEthFilterBuilder(). + FromBlockEpoch(0). + Topic1OneOf(paddedEthHash(EventMatrixContract.Ev["EventTwoIndexedWithData"]), paddedEthHash(EventMatrixContract.Ev["EventOneIndexed"])). + Topic2OneOf(paddedEthHash(paddedUint64(44))). + 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["EventOneIndexed"], + paddedUint64(44), + }, + Data: nil, + }, + }, + }, + } + + 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) + }) + } +} + +func TestEthGetLogsWithBlockRanges(t *testing.T) { + require := require.New(t) + kit.QuietAllLogsExcept("events", "messagepool") + + blockTime := 100 * time.Millisecond + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Set up the test fixture with a standard list of invocations + _, _, messages := invokeEventMatrix(ctx, t, client) + + // Organize expected logs into three partitions for range testing + expectedByHeight := map[abi.ChainEpoch][]ExpectedEthLog{} + distinctHeights := map[abi.ChainEpoch]bool{} + + // Select events for partitioning + for _, m := range messages { + if bytes.Equal(m.invocation.Selector, EventMatrixContract.Fn["logEventTwoIndexedWithData"]) { + addr := getContractEthAddress(ctx, t, client, m.invocation.Target) + args := unpackUint64Values(m.invocation.Data) + require.Equal(3, len(args), "logEventTwoIndexedWithData should have 3 arguments") + + distinctHeights[m.ts.Height()] = true + expectedByHeight[m.ts.Height()] = append(expectedByHeight[m.ts.Height()], ExpectedEthLog{ + Address: addr, + Topics: []ethtypes.EthBytes{ + EventMatrixContract.Ev["EventTwoIndexedWithData"], + paddedUint64(args[0]), + paddedUint64(args[1]), + }, + Data: paddedUint64(args[2]), + }) + } + } + + // Divide heights into 3 partitions, they don't have to be equal + require.True(len(distinctHeights) >= 3, "expected slice should divisible into three partitions") + heights := make([]abi.ChainEpoch, 0, len(distinctHeights)) + for h := range distinctHeights { + heights = append(heights, h) + } + sort.Slice(heights, func(i, j int) bool { + return heights[i] < heights[j] + }) + heightsPerPartition := len(heights) / 3 + + type partition struct { + start abi.ChainEpoch + end abi.ChainEpoch + expected []ExpectedEthLog + } + + var partition1, partition2, partition3 partition + + partition1.start = heights[0] + partition1.end = heights[heightsPerPartition-1] + for e := partition1.start; e <= partition1.end; e++ { + exp, ok := expectedByHeight[e] + if !ok { + continue + } + partition1.expected = append(partition1.expected, exp...) + } + t.Logf("partition1 from %d to %d with %d expected", partition1.start, partition1.end, len(partition1.expected)) + require.True(len(partition1.expected) > 0, "partition should have events") + + partition2.start = heights[heightsPerPartition] + partition2.end = heights[heightsPerPartition*2-1] + for e := partition2.start; e <= partition2.end; e++ { + exp, ok := expectedByHeight[e] + if !ok { + continue + } + partition2.expected = append(partition2.expected, exp...) + } + t.Logf("partition2 from %d to %d with %d expected", partition2.start, partition2.end, len(partition2.expected)) + require.True(len(partition2.expected) > 0, "partition should have events") + + partition3.start = heights[heightsPerPartition*2] + partition3.end = heights[len(heights)-1] + for e := partition3.start; e <= partition3.end; e++ { + exp, ok := expectedByHeight[e] + if !ok { + continue + } + partition3.expected = append(partition3.expected, exp...) + } + t.Logf("partition3 from %d to %d with %d expected", partition3.start, partition3.end, len(partition3.expected)) + require.True(len(partition3.expected) > 0, "partition should have events") + + // these are the topics we selected for partitioning earlier + topics := []ethtypes.EthHash{paddedEthHash(EventMatrixContract.Ev["EventTwoIndexedWithData"])} + + union := func(lists ...[]ExpectedEthLog) []ExpectedEthLog { + ret := []ExpectedEthLog{} + for _, list := range lists { + ret = append(ret, list...) + } + return ret + } + + testCases := []struct { + name string + spec *ethtypes.EthFilterSpec + expected []ExpectedEthLog + }{ + { + name: "find all events from genesis", + spec: newEthFilterBuilder().FromBlockEpoch(0).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected, partition2.expected, partition3.expected), + }, + + { + name: "find all from start of partition1", + spec: newEthFilterBuilder().FromBlockEpoch(partition1.start).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected, partition2.expected, partition3.expected), + }, + + { + name: "find all from start of partition2", + spec: newEthFilterBuilder().FromBlockEpoch(partition2.start).Topic1OneOf(topics...).Filter(), + expected: union(partition2.expected, partition3.expected), + }, + + { + name: "find all from start of partition3", + spec: newEthFilterBuilder().FromBlockEpoch(partition3.start).Topic1OneOf(topics...).Filter(), + expected: union(partition3.expected), + }, + + { + name: "find none after end of partition3", + spec: newEthFilterBuilder().FromBlockEpoch(partition3.end + 1).Topic1OneOf(topics...).Filter(), + expected: nil, + }, + + { + name: "find all events from genesis to end of partition1", + spec: newEthFilterBuilder().FromBlockEpoch(0).ToBlockEpoch(partition1.end).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected), + }, + + { + name: "find all events from genesis to end of partition2", + spec: newEthFilterBuilder().FromBlockEpoch(0).ToBlockEpoch(partition2.end).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected, partition2.expected), + }, + + { + name: "find all events from genesis to end of partition3", + spec: newEthFilterBuilder().FromBlockEpoch(0).ToBlockEpoch(partition3.end).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected, partition2.expected, partition3.expected), + }, + + { + name: "find none from genesis to start of partition1", + spec: newEthFilterBuilder().FromBlockEpoch(0).ToBlockEpoch(partition1.start - 1).Topic1OneOf(topics...).Filter(), + expected: nil, + }, + + { + name: "find all events in partition1", + spec: newEthFilterBuilder().FromBlockEpoch(partition1.start).ToBlockEpoch(partition1.end).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected), + }, + + { + name: "find all events in partition2", + spec: newEthFilterBuilder().FromBlockEpoch(partition2.start).ToBlockEpoch(partition2.end).Topic1OneOf(topics...).Filter(), + expected: union(partition2.expected), + }, + + { + name: "find all events in partition3", + spec: newEthFilterBuilder().FromBlockEpoch(partition3.start).ToBlockEpoch(partition3.end).Topic1OneOf(topics...).Filter(), + expected: union(partition3.expected), + }, + + { + name: "find all events from earliest to end of partition1", + spec: newEthFilterBuilder().FromBlock("earliest").ToBlockEpoch(partition1.end).Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected), + }, + + { + name: "find all events from start of partition3 to latest", + spec: newEthFilterBuilder().FromBlockEpoch(partition3.start).ToBlock("latest").Topic1OneOf(topics...).Filter(), + expected: union(partition3.expected), + }, + + { + name: "find all events from earliest to latest", + spec: newEthFilterBuilder().FromBlock("earliest").ToBlock("latest").Topic1OneOf(topics...).Filter(), + expected: union(partition1.expected, partition2.expected, partition3.expected), + }, + } + + 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 { + invocation Invocation // the solidity invocation that generated this message + msg api.Message + events []types.Event // events extracted from receipt + ts *types.TipSet + reverted bool +} + +func getContractEthAddress(ctx context.Context, t *testing.T, client *kit.TestFullNode, addr address.Address) ethtypes.EthAddress { + head, err := client.ChainHead(ctx) + require.NoError(t, err) + + actor, err := client.StateGetActor(ctx, addr, 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 { @@ -256,14 +1355,12 @@ func TestEthNewFilterCatchAll(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 } } @@ -272,62 +1369,310 @@ func TestEthNewFilterCatchAll(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) + eventMap := map[cid.Cid][]types.Event{} + invocationMap := map[cid.Cid]Invocation{} + 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") + + invocationMap[ret.Message] = inv + + require.NotNil(t, ret.Receipt.EventsRoot, "no event root on receipt") + + evs := client.EVM().LoadEvents(ctx, *ret.Receipt.EventsRoot) + eventMap[ret.Message] = evs } select { case <-waitAllCh: - case <-time.After(time.Minute): - t.Errorf("timeout to wait for pack messages") + case <-ctx.Done(): + t.Fatalf("timeout waiting to pack messages") } received := make(map[ethtypes.EthHash]msgInTipset) for m := range msgChan { + inv, ok := invocationMap[m.msg.Cid] + require.True(ok) + m.invocation = inv + + evs, ok := eventMap[m.msg.Cid] + require.True(ok) + m.events = evs + eh, err := client.EthGetTransactionHashByCid(ctx, 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") - ts, err := client.ChainHead(ctx) - require.NoError(err) + return received +} - actor, err := client.StateGetActor(ctx, idAddr, ts.Key()) - require.NoError(err) - require.NotNil(actor.Address) - ethContractAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address) - require.NoError(err) +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() - // collect filter results - res, err := client.EthGetFilterChanges(ctx, filterID) - require.NoError(err) + fromAddr, idAddr := client.EVM().DeployContractFromFilename(ctx, EventsContract.Filename) - // expect to have seen iteration number of events - require.Equal(iterations, len(res.Results)) + invocations := make([]Invocation, iterations) + for i := range invocations { + invocations[i] = Invocation{ + Sender: fromAddr, + Target: idAddr, + Selector: EventsContract.Fn["log_four_data"], + Data: nil, + } + } - 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})) + messages := invokeAndWaitUntilAllOnChain(t, client, invocations) - 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") + ethAddr := getContractEthAddress(ctx, t, client, idAddr) - elog, err := ParseEthLog(rc) - require.NoError(err) + return ethAddr, messages +} - require.Equal(ethContractAddr, elog.Address, "event address") - require.Equal(ethtypes.EthUint64(0), elog.TransactionIndex, "transaction index") // only one message per tipset +func invokeEventMatrix(ctx context.Context, t *testing.T, client *kit.TestFullNode) (ethtypes.EthAddress, ethtypes.EthAddress, map[ethtypes.EthHash]msgInTipset) { + sender1, contract1 := client.EVM().DeployContractFromFilename(ctx, EventMatrixContract.Filename) + sender2, contract2 := client.EVM().DeployContractFromFilename(ctx, EventMatrixContract.Filename) - msg, exists := received[elog.TransactionHash] + 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: sender2, + 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), + }, + // log EventTwoIndexed(44,19) from contract1 + // topic1: hash(EventTwoIndexed) + // topic2: 44 + // topic3: 19 + { + Sender: sender1, + Target: contract1, + Selector: EventMatrixContract.Fn["logEventTwoIndexed"], + Data: packUint64Values(40, 20), + }, + } + + 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() @@ -336,17 +1681,113 @@ func TestEthNewFilterCatchAll(t *testing.T) { tsCidHash, err := ethtypes.EthHashFromCid(tsCid) require.NoError(err) - require.Equal(tsCidHash, elog.BlockHash, "block hash") + require.Equal(tsCidHash, elog.BlockHash, "block hash matches tipset key") - 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") + // 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 + } - require.Equal(data1, elog.Data, "data1") + 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(fmt.Sprintf("found unexpected log at height %d:\n", msg.ts.Height())) + 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 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) { @@ -462,346 +1903,7 @@ 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 - - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) - ens.InterconnectAll().BeginMining(blockTime) - - ethContractAddr, received := invokeContractAndWaitUntilAllOnChain(t, client, 10) - - 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})) - - pstring := func(s string) *string { return &s } - - // get all logs - res, err := client.EthGetLogs(context.Background(), ðtypes.EthFilterSpec{ - FromBlock: pstring("0x0"), - }) - 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") - - } -} - -func TestEthGetLogsByTopic(t *testing.T) { - require := require.New(t) - - kit.QuietMiningLogs() - - blockTime := 100 * time.Millisecond - - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) - 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})) - - // find log by known topic1 - var spec ethtypes.EthFilterSpec - err := json.Unmarshal([]byte(`{"fromBlock":"0x0","topics":["0x0000000000000000000000000000000000000000000000000000000000001111"]}`), &spec) - require.NoError(err) - - 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") - - } -} - -func TestEthSubscribeLogs(t *testing.T) { - require := require.New(t) - - kit.QuietMiningLogs() - - blockTime := 100 * time.Millisecond - client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC()) - ens.InterconnectAll().BeginMining(blockTime) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - // 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) - - // install filter - respCh, err := client.EthSubscribe(ctx, "logs", nil) - require.NoError(err) - - subResponses := []ethtypes.EthSubscriptionResponse{} - go func() { - for resp := range respCh { - subResponses = append(subResponses, resp) - } - }() - - const iterations = 10 - - type msgInTipset struct { - msg api.Message - ts *types.TipSet - } - - 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}: - 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") - } - - if len(subResponses) > 0 { - ok, err := client.EthUnsubscribe(ctx, subResponses[0].SubscriptionID) - require.NoError(err) - require.True(ok, "unsubscribed") - } - - 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") - - // expect to have seen all logs - require.Equal(len(received), len(subResponses)) -} - -func leftpad32(orig []byte) []byte { +func paddedEthBytes(orig []byte) ethtypes.EthBytes { needed := 32 - len(orig) if needed <= 0 { return orig @@ -810,3 +1912,136 @@ 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 unpackUint64Values(data []byte) []uint64 { + if len(data)%32 != 0 { + panic("data length not a multiple of 32") + } + + var vals []uint64 + for i := 0; i < len(data); i += 32 { + v := binary.BigEndian.Uint64(data[i+24 : i+32]) + vals = append(vals, v) + } + return vals +} + +func newEthFilterBuilder() *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 abi.ChainEpoch) *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 abi.ChainEpoch) *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 +} + +func decodeLogBytes(orig []byte) []byte { + if len(orig) == 0 { + return orig + } + decoded, err := cbg.ReadByteArray(bytes.NewReader(orig), uint64(len(orig))) + if err != nil { + return orig + } + return decoded +} diff --git a/itests/kit/evm.go b/itests/kit/evm.go index be7169b47..46aaf52db 100644 --- a/itests/kit/evm.go +++ b/itests/kit/evm.go @@ -83,6 +83,9 @@ func (e *EVM) DeployContractFromFilename(ctx context.Context, binFilename string contractHex, err := os.ReadFile(binFilename) require.NoError(e.t, err) + // strip any trailing newlines from the file + contractHex = bytes.TrimRight(contractHex, "\n") + contract, err := hex.DecodeString(string(contractHex)) require.NoError(e.t, err) diff --git a/itests/kit/log.go b/itests/kit/log.go index beac3895c..0da9adfeb 100644 --- a/itests/kit/log.go +++ b/itests/kit/log.go @@ -1,6 +1,9 @@ package kit import ( + "io" + "log" + logging "github.com/ipfs/go-log/v2" "github.com/filecoin-project/lotus/lib/lotuslog" @@ -20,3 +23,13 @@ func QuietMiningLogs() { _ = logging.SetLogLevel("rpc", "ERROR") _ = logging.SetLogLevel("dht/RtRefreshManager", "ERROR") } + +func QuietAllLogsExcept(names ...string) { + log.SetOutput(io.Discard) // suppress LogDatastore messages + + lotuslog.SetupLogLevels() + logging.SetAllLoggers(logging.LevelError) + for _, name := range names { + _ = logging.SetLogLevel(name, "INFO") + } +} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 1cf3475de..7eb992a22 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -952,18 +952,20 @@ func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *ethtype // Here the client is looking for events between the head and some future height ts := e.Chain.GetHeaviestTipSet() if maxHeight-ts.Height() > e.MaxFilterHeightRange { - return nil, xerrors.Errorf("invalid epoch range") + return nil, xerrors.Errorf("invalid epoch range: to block is too far in the future (maximum: %d)", e.MaxFilterHeightRange) } } else if minHeight >= 0 && maxHeight == -1 { // Here the client is looking for events between some time in the past and the current head ts := e.Chain.GetHeaviestTipSet() if ts.Height()-minHeight > e.MaxFilterHeightRange { - return nil, xerrors.Errorf("invalid epoch range") + return nil, xerrors.Errorf("invalid epoch range: from block is too far in the past (maximum: %d)", e.MaxFilterHeightRange) } } else if minHeight >= 0 && maxHeight >= 0 { - if minHeight > maxHeight || maxHeight-minHeight > e.MaxFilterHeightRange { - return nil, xerrors.Errorf("invalid epoch range") + if minHeight > maxHeight { + return nil, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", minHeight, maxHeight) + } else if maxHeight-minHeight > e.MaxFilterHeightRange { + return nil, xerrors.Errorf("invalid epoch range: range between to and from blocks is too large (maximum: %d)", e.MaxFilterHeightRange) } } @@ -979,13 +981,16 @@ func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *ethtype } for idx, vals := range filterSpec.Topics { + if len(vals) == 0 { + continue + } // Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4 key := fmt.Sprintf("topic%d", idx+1) - keyvals := make([][]byte, len(vals)) - for i, v := range vals { - keyvals[i] = v[:] + for _, v := range vals { + buf := make([]byte, len(v[:])) + copy(buf, v[:]) + keys[key] = append(keys[key], buf) } - keys[key] = keyvals } return e.EventFilterManager.Install(ctx, minHeight, maxHeight, tipsetCid, addresses, keys) @@ -1228,7 +1233,7 @@ func ethFilterResultFromEvents(evs []*filter.CollectedEvent, sa StateAPI) (*etht 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 { @@ -1771,7 +1776,7 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLook } for _, entry := range evt.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 { l.Topics = append(l.Topics, value) } else { @@ -1882,21 +1887,6 @@ func EthTxHashGC(ctx context.Context, retentionDays int, manager *EthTxHashManag } } -// decodeLogBytes decodes a CBOR-serialized array into its original form. -// -// This function swallows errors and returns the original array if it failed -// to decode. -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 -} - // TODO we could also emit full EVM words from the EVM runtime, but not doing so // makes the contract slightly cheaper (and saves storage bytes), at the expense // of having to left pad in the API, which is a pretty acceptable tradeoff at