Skip to content

Commit

Permalink
fix: Eth Event Receipt Logs: use event index for logs (#12269)
Browse files Browse the repository at this point in the history
* use event index for receipt logs

* tests

* increment event count if there is an address mismatch

* itests for receipt logs consistency

* update ChangeLog

* changes as per review

* do not increment event count if address resolution fails

* Apply suggestions from code review

Co-authored-by: Rod Vagg <[email protected]>

* address review

---------

Co-authored-by: Rod Vagg <[email protected]>
  • Loading branch information
aarshkshah1992 and rvagg authored Jul 23, 2024
1 parent 6994580 commit 4f63a08
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- https://github.com/filecoin-project/lotus/pull/12221: Fix a nil reference panic in the ETH Trace API
- https://github.com/filecoin-project/lotus/pull/12112: Moved consts from build/ to build/buildconstants/ for ligher curio deps.
- https://github.com/filecoin-project/lotus/pull/12237: Upgrade to go-f3 `v0.0.4`.
- https://github.com/filecoin-project/lotus/pull/12269 Fix `logIndex` ordering in `EthGetTransactionReceipt` by using the EventIndex to fetch logs

## ☢️ Upgrade Warnings ☢️

Expand Down
1 change: 1 addition & 0 deletions itests/contracts/MultipleEvents.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6080604052348015600e575f80fd5b5061058b8061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c806305734db71461003857806398e8da0014610054575b5f80fd5b610052600480360381019061004d91906102c0565b61005e565b005b61005c61013a565b005b8173ffffffffffffffffffffffffffffffffffffffff167fd76645dad5a7864a79954383dacf5c3f9cf3bf86d0a21241fcbae7b08cbbcb9660405160405180910390a2807f42d45d4e904b929a49443ad704c27633daae11d6ff7a0d0dedc9dbe70004d7d76040516100cf90610358565b60405180910390a27f5c362dbc3ba7b340d21482342acacf25f578611b2e5a4f136ff1a280ac0d9408828260405160200161010b9291906103db565b6040516020818303038152906040528051906020012060405161012e919061041e565b60405180910390a15050565b3373ffffffffffffffffffffffffffffffffffffffff167fb30247b59cead5e1c696f81be8486379fbc9d20feae266f6df16fcc2143ad355426040516101809190610446565b60405180910390a27f0c49df121e06fc3a0f60cfc2c9b92fce5c5232e223b63a51a780d4e8aae0132d6040516101b5906104a9565b60405180910390a17f38ee5a08acae32a0ccec0eef68b73ba44f4b09e2f3df37062af8e885a7fd23af602a6040516101ed9190610509565b60405180910390a17fd0916c673494f5a60c9f5d12c864ebec8398c930a7f2b57e0665c2feb63448846001604051610225919061053c565b60405180910390a1565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61025c82610233565b9050919050565b61026c81610252565b8114610276575f80fd5b50565b5f8135905061028781610263565b92915050565b5f819050919050565b61029f8161028d565b81146102a9575f80fd5b50565b5f813590506102ba81610296565b92915050565b5f80604083850312156102d6576102d561022f565b5b5f6102e385828601610279565b92505060206102f4858286016102ac565b9150509250929050565b5f82825260208201905092915050565b7f5468726565206576656e74732066756e6374696f6e2063616c6c6564000000005f82015250565b5f610342601c836102fe565b915061034d8261030e565b602082019050919050565b5f6020820190508181035f83015261036f81610336565b9050919050565b5f8160601b9050919050565b5f61038c82610376565b9050919050565b5f61039d82610382565b9050919050565b6103b56103b082610252565b610393565b82525050565b5f819050919050565b6103d56103d08261028d565b6103bb565b82525050565b5f6103e682856103a4565b6014820191506103f682846103c4565b6020820191508190509392505050565b5f819050919050565b61041881610406565b82525050565b5f6020820190506104315f83018461040f565b92915050565b6104408161028d565b82525050565b5f6020820190506104595f830184610437565b92915050565b7f466f7572206576656e74732066756e6374696f6e2063616c6c656400000000005f82015250565b5f610493601b836102fe565b915061049e8261045f565b602082019050919050565b5f6020820190508181035f8301526104c081610487565b9050919050565b5f819050919050565b5f819050919050565b5f6104f36104ee6104e9846104c7565b6104d0565b61028d565b9050919050565b610503816104d9565b82525050565b5f60208201905061051c5f8301846104fa565b92915050565b5f8115159050919050565b61053681610522565b82525050565b5f60208201905061054f5f83018461052d565b9291505056fea26469706673582212207d4001220d45b88d2efb387a0bb0c06f00739212df2c528db0db9850a48046c064736f6c63430008190033
28 changes: 28 additions & 0 deletions itests/contracts/MultipleEvents.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17;

contract MultipleEventEmitter {
// Define events
event Event1(address indexed sender, uint256 timestamp);
event Event2(string message);
event Event3(uint256 value);
event Event4(bool flag);
event Event5(address indexed recipient);
event Event6(uint256 indexed id, string data);
event Event7(bytes32 hash);

// Function that emits four events
function emitFourEvents() public {
emit Event1(msg.sender, block.timestamp);
emit Event2("Four events function called");
emit Event3(42);
emit Event4(true);
}

// Function that emits three events
function emitThreeEvents(address _recipient, uint256 _id) public {
emit Event5(_recipient);
emit Event6(_id, "Three events function called");
emit Event7(keccak256(abi.encodePacked(_recipient, _id)));
}
}
207 changes: 207 additions & 0 deletions itests/eth_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/filecoin-project/go-state-types/big"

"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/store"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
Expand Down Expand Up @@ -509,6 +510,20 @@ func TestEthGetLogsBasic(t *testing.T) {
elogs, err := parseEthLogsFromFilterResult(res)
require.NoError(err)
AssertEthLogs(t, elogs, expected, received)

require.Len(elogs, 1)
rct, err := client.EthGetTransactionReceipt(ctx, elogs[0].TransactionHash)
require.NoError(err)
require.NotNil(rct)

require.Len(rct.Logs, 1)
var rctLogs []*ethtypes.EthLog
for _, rctLog := range rct.Logs {
addr := &rctLog
rctLogs = append(rctLogs, addr)
}

AssertEthLogs(t, rctLogs, expected, received)
}

func TestEthSubscribeLogsNoTopicSpec(t *testing.T) {
Expand Down Expand Up @@ -622,6 +637,145 @@ func TestTxReceiptBloom(t *testing.T) {
require.Equal(t, 6, bitsSet)
}

func TestMultipleEvents(t *testing.T) {
blockTime := 500 * 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()

// create a new Ethereum account
key, ethAddr, deployer := client.EVM().NewAccount()
// send some funds to the f410 address
kit.SendFunds(ctx, t, client, deployer, types.FromFil(10))

// DEPLOY CONTRACT
tx := deployContractWithEthTx(ctx, t, client, ethAddr, "./contracts/MultipleEvents.hex")

client.EVM().SignTransaction(tx, key.PrivateKey)
hash := client.EVM().SubmitTransaction(ctx, tx)

receipt, err := client.EVM().WaitTransaction(ctx, hash)
require.NoError(t, err)
require.NotNil(t, receipt)
require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status)

// invoke function to emit four events
contractAddr := client.EVM().ComputeContractAddress(ethAddr, 0)

// https://abi.hashex.org/
params4Events, err := hex.DecodeString("98e8da00")
require.NoError(t, err)

params3Events, err := hex.DecodeString("05734db70000000000000000000000001cd1eeecac00fe01f9d11803e48c15c478fe1d22000000000000000000000000000000000000000000000000000000000000000b")
require.NoError(t, err)

gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
From: &ethAddr,
To: &contractAddr,
Data: params4Events,
}})
require.NoError(t, err)

gaslimit, err := client.EthEstimateGas(ctx, gasParams)
require.NoError(t, err)

maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)

paramsArray := [][]byte{params4Events, params3Events, params3Events}

var receipts []*api.EthTxReceipt
var hashes []ethtypes.EthHash

nonce := 1
for {
receipts = nil
hashes = nil

for _, params := range paramsArray {
invokeTx := ethtypes.Eth1559TxArgs{
ChainID: build.Eip155ChainId,
To: &contractAddr,
Value: big.Zero(),
Nonce: nonce,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
Input: params,
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}

// Submit transaction with valid signature
client.EVM().SignTransaction(&invokeTx, key.PrivateKey)
hash := client.EVM().SubmitTransaction(ctx, &invokeTx)
hashes = append(hashes, hash)
nonce++
require.True(t, nonce <= 10, "tried too many times to land messages in same tipset")
}

for _, hash := range hashes {
receipt, err := client.EVM().WaitTransaction(ctx, hash)
require.NoError(t, err)
require.NotNil(t, receipt)
receipts = append(receipts, receipt)
}

// Check if all receipts are in the same tipset
allInSameTipset := true
for i := 1; i < len(receipts); i++ {
if receipts[i].BlockHash != receipts[0].BlockHash {
allInSameTipset = false
break
}
}

if allInSameTipset {
break
}
}

// Assert that first receipt has 4 event logs and the other two have 3 each
require.Equal(t, 4, len(receipts[0].Logs), "First transaction should have 4 event logs")
require.Equal(t, 3, len(receipts[1].Logs), "Second transaction should have 3 event logs")
require.Equal(t, 3, len(receipts[2].Logs), "Third transaction should have 3 event logs")

// Iterate over all logs in all receipts and assert that the logIndex field is numbered from 0 to 9
expectedLogIndex := 0
var receiptLogs []ethtypes.EthLog
for _, receipt := range receipts {
for _, log := range receipt.Logs {
require.Equal(t, ethtypes.EthUint64(expectedLogIndex), log.LogIndex, "LogIndex should be sequential")
expectedLogIndex++
receiptLogs = append(receiptLogs, log)
}
}
require.Equal(t, 10, expectedLogIndex, "Total number of logs should be 10")

// assert that all logs match b/w ethGetLogs and receipts
res, err := client.EthGetLogs(ctx, &ethtypes.EthFilterSpec{
BlockHash: &receipts[0].BlockHash,
})
require.NoError(t, err)

logs, err := parseEthLogsFromFilterResult(res)
// Convert []*EthLog to []EthLog
ethLogs := make([]ethtypes.EthLog, len(logs))
for i, log := range logs {
if log != nil {
ethLogs[i] = *log
}
}
require.NoError(t, err)
require.Equal(t, 10, len(logs), "Total number of logs should be 10")

require.Equal(t, receiptLogs, ethLogs, "Logs from ethGetLogs and receipts should match")
}

func TestEthGetLogs(t *testing.T) {
require := require.New(t)
kit.QuietAllLogsExcept("events", "messagepool")
Expand Down Expand Up @@ -649,6 +803,20 @@ func TestEthGetLogs(t *testing.T) {
elogs, err := parseEthLogsFromFilterResult(res)
require.NoError(err)
AssertEthLogs(t, elogs, tc.expected, messages)

for _, elog := range elogs {
rct, err := client.EthGetTransactionReceipt(ctx, elog.TransactionHash)
require.NoError(err)
require.NotNil(rct)
require.Len(rct.Logs, 1)
require.EqualValues(rct.Logs[0].LogIndex, elog.LogIndex)
require.EqualValues(rct.Logs[0].BlockNumber, elog.BlockNumber)
require.EqualValues(rct.Logs[0].TransactionIndex, elog.TransactionIndex)
require.EqualValues(rct.Logs[0].TransactionHash, elog.TransactionHash)
require.EqualValues(rct.Logs[0].Data, elog.Data)
require.EqualValues(rct.Logs[0].Topics, elog.Topics)
require.EqualValues(rct.Logs[0].Address, elog.Address)
}
})
}
}
Expand Down Expand Up @@ -2349,3 +2517,42 @@ func unpackUint64Values(data []byte) []uint64 {
}
return vals
}

// deployContractWithEthTx returns an Ethereum transaction that can be used to deploy the smart contract at "contractPath" on chain
// It is different from `DeployContractFromFilename` because it used an Ethereum transaction to deploy the contract whereas the
// latter uses a FIL transaction.
func deployContractWithEthTx(ctx context.Context, t *testing.T, client *kit.TestFullNode, ethAddr ethtypes.EthAddress,
contractPath string) *ethtypes.Eth1559TxArgs {
// install contract
contractHex, err := os.ReadFile(contractPath)
require.NoError(t, err)

contract, err := hex.DecodeString(string(contractHex))
require.NoError(t, err)

gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
From: &ethAddr,
Data: contract,
}})
require.NoError(t, err)

gaslimit, err := client.EthEstimateGas(ctx, gasParams)
require.NoError(t, err)

maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)

// now deploy a contract from the embryo, and validate it went well
return &ethtypes.Eth1559TxArgs{
ChainID: build.Eip155ChainId,
Value: big.Zero(),
Nonce: 0,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
Input: contract,
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}
}
3 changes: 2 additions & 1 deletion node/builder_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ func ConfigFullNode(c interface{}) Option {
// in lite-mode Eth api is provided by gateway
ApplyIf(isFullNode,
If(cfg.Fevm.EnableEthRPC,
Override(new(*full.EthEventHandler), modules.EthEventHandler(cfg.Events, cfg.Fevm.EnableEthRPC)),
Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.Fevm)),
Override(new(full.EthEventAPI), modules.EthEventHandler(cfg.Events, cfg.Fevm.EnableEthRPC)),
Override(new(full.EthEventAPI), From(new(*full.EthEventHandler))),
),
If(!cfg.Fevm.EnableEthRPC,
Override(new(full.EthModuleAPI), &full.EthModuleDummy{}),
Expand Down
40 changes: 35 additions & 5 deletions node/impl/full/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type EthModule struct {
Mpool *messagepool.MessagePool
StateManager *stmgr.StateManager
EthTxHashManager *EthTxHashManager
EthEventHandler *EthEventHandler

ChainAPI
MpoolAPI
Expand Down Expand Up @@ -432,7 +433,7 @@ func (a *EthModule) EthGetTransactionReceiptLimited(ctx context.Context, txHash
return nil, xerrors.Errorf("failed to convert %s into an Eth Txn: %w", txHash, err)
}

receipt, err := newEthTxReceipt(ctx, tx, msgLookup, a.ChainAPI, a.StateAPI)
receipt, err := newEthTxReceipt(ctx, tx, msgLookup, a.ChainAPI, a.StateAPI, a.EthEventHandler)
if err != nil {
return nil, xerrors.Errorf("failed to convert %s into an Eth Receipt: %w", txHash, err)
}
Expand Down Expand Up @@ -1270,7 +1271,36 @@ func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam e
return ethtypes.EthBytes{}, nil
}

// TODO: For now, we're fetching logs from the index for the entire block and then filtering them by the transaction hash
// This allows us to use the current schema of the event Index DB that has been optimised to use the "tipset_key_cid" index
// However, this can be replaced to filter logs in the event Index DB by the "msgCid" if we pass it down to the query generator
func (e *EthEventHandler) getEthLogsForBlockAndTransaction(ctx context.Context, blockHash *ethtypes.EthHash, txHash ethtypes.EthHash) ([]ethtypes.EthLog, error) {
ces, err := e.ethGetEventsForFilter(ctx, &ethtypes.EthFilterSpec{BlockHash: blockHash})
if err != nil {
return nil, err
}
logs, err := ethFilterLogsFromEvents(ctx, ces, e.SubManager.StateAPI)
if err != nil {
return nil, err
}
var out []ethtypes.EthLog
for _, log := range logs {
if log.TransactionHash == txHash {
out = append(out, log)
}
}
return out, nil
}

func (e *EthEventHandler) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) {
ces, err := e.ethGetEventsForFilter(ctx, filterSpec)
if err != nil {
return nil, err
}
return ethFilterResultFromEvents(ctx, ces, e.SubManager.StateAPI)
}

func (e *EthEventHandler) ethGetEventsForFilter(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) ([]*filter.CollectedEvent, error) {
if e.EventFilterManager == nil {
return nil, api.ErrNotSupported
}
Expand All @@ -1279,7 +1309,7 @@ func (e *EthEventHandler) EthGetLogs(ctx context.Context, filterSpec *ethtypes.E
return nil, xerrors.Errorf("cannot use eth_get_logs if historical event index is disabled")
}

pf, err := e.parseEthFilterSpec(ctx, filterSpec)
pf, err := e.parseEthFilterSpec(filterSpec)
if err != nil {
return nil, xerrors.Errorf("failed to parse eth filter spec: %w", err)
}
Expand Down Expand Up @@ -1340,7 +1370,7 @@ func (e *EthEventHandler) EthGetLogs(ctx context.Context, filterSpec *ethtypes.E

_ = e.uninstallFilter(ctx, f)

return ethFilterResultFromEvents(ctx, ces, e.SubManager.StateAPI)
return ces, nil
}

// note that we can have null blocks at the given height and the event Index is not null block aware
Expand Down Expand Up @@ -1492,7 +1522,7 @@ type parsedFilter struct {
keys map[string][]types.ActorEventBlock
}

func (e *EthEventHandler) parseEthFilterSpec(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*parsedFilter, error) {
func (e *EthEventHandler) parseEthFilterSpec(filterSpec *ethtypes.EthFilterSpec) (*parsedFilter, error) {
var (
minHeight abi.ChainEpoch
maxHeight abi.ChainEpoch
Expand Down Expand Up @@ -1556,7 +1586,7 @@ func (e *EthEventHandler) EthNewFilter(ctx context.Context, filterSpec *ethtypes
return ethtypes.EthFilterID{}, api.ErrNotSupported
}

pf, err := e.parseEthFilterSpec(ctx, filterSpec)
pf, err := e.parseEthFilterSpec(filterSpec)
if err != nil {
return ethtypes.EthFilterID{}, err
}
Expand Down
Loading

0 comments on commit 4f63a08

Please sign in to comment.