Repository navigation
fix(evmonly): continue block execution past stale nonces #4229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c6a67a
80ed65f
b8c89d8
1ef2cc6
808973c
f3299f3
2d592da
ea5be79
cfb744c
6fcb009
99797fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,8 +79,7 @@ func resolveBlockByNumber(ctx context.Context, backend Backend, number ethrpc.Bl | |
| // encodeBlock renders block as an eth_getBlockBy* response. nonce, mixHash, | ||
| // sha3Uncles, difficulty, extraData, uncles, and totalDifficulty are always | ||
| // their Ethereum-inapplicable zero value, matching evmrpc's v2 encoder. | ||
| // logsBloom is always zero: unlike v2, this execution path does not read a | ||
| // receipt per transaction to aggregate one. | ||
| // logsBloom is always zero; receipt blooms are not aggregated here. | ||
| func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlock, fullTx bool) (map[string]any, error) { | ||
| number := block.Block.Height | ||
| blockHash := common.BytesToHash(block.BlockID.Hash) | ||
|
|
@@ -99,7 +98,6 @@ func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlo | |
|
|
||
| txs := block.Block.Txs | ||
| transactions := make([]any, len(txs)) | ||
| var lastReceipt *evmtypes.Receipt | ||
| if fullTx { | ||
| chainConfig, err := api.backend.EvmChainConfig() | ||
| if err != nil { | ||
|
|
@@ -114,7 +112,7 @@ func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlo | |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| stored, err := api.receiptFor(ctx, ethtx.Hash()) | ||
| stored, err := receiptFor(ctx, api.store, ethtx.Hash()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read transaction receipt at block %d index %d: %w", number, i, err) | ||
| } | ||
|
|
@@ -123,37 +121,19 @@ func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlo | |
| replaceFrom(result, stored) | ||
| } | ||
| transactions[i] = result | ||
| if i == len(txs)-1 { | ||
| lastReceipt = stored | ||
| } | ||
| } | ||
| } else { | ||
| for i, raw := range txs { | ||
| ethtx, err := decodeBlockTx(raw, number, i) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| hash := ethtx.Hash() | ||
| transactions[i] = hash | ||
| if i == len(txs)-1 { | ||
| lastReceipt, err = api.receiptFor(ctx, hash) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read last transaction receipt for block %d: %w", number, err) | ||
| } | ||
| } | ||
| transactions[i] = ethtx.Hash() | ||
| } | ||
| } | ||
| // The last transaction's CumulativeGasUsed already equals the whole | ||
| // block's gas used; summing every transaction's own GasUsed would need a | ||
| // receipt per transaction instead of one. | ||
| // | ||
| // This total is scoped to the one Autobahn lane this block belongs to, | ||
| // not every lane executing concurrently at this point in the chain. | ||
| // Revisit once superblocks merge lanes into a single block; punted for | ||
| // now since a block today is exactly one lane's transactions. | ||
| var gasUsed hexutil.Uint64 | ||
| if lastReceipt != nil && lastReceipt.BlockNumber == uint64(number) { //nolint:gosec // G115: number is a validated block height. | ||
| gasUsed = hexutil.Uint64(lastReceipt.CumulativeGasUsed) | ||
| gasUsed, err := blockGasUsed(ctx, api.store, block) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| result := map[string]any{ | ||
|
|
@@ -169,7 +149,7 @@ func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlo | |
| "difficulty": (*hexutil.Big)(big.NewInt(0)), // inapplicable to Sei | ||
| "extraData": hexutil.Bytes{}, // inapplicable to Sei | ||
| "gasLimit": hexutil.Uint64(gasLimit), | ||
| "gasUsed": gasUsed, | ||
| "gasUsed": hexutil.Uint64(gasUsed), | ||
| "timestamp": hexutil.Uint64(blockUnix), | ||
| "milliTimestamp": hexutil.Uint64(block.Block.Time.UnixMilli()), //nolint:gosec // G115: block timestamps are positive. | ||
| "transactionsRoot": common.BytesToHash(block.Block.DataHash), | ||
|
|
@@ -187,8 +167,8 @@ func (api *blockAPI) encodeBlock(ctx context.Context, block *coretypes.ResultBlo | |
|
|
||
| // receiptFor returns hash's stored receipt, or nil with a nil error when no | ||
| // receipt is stored for it. | ||
| func (api *blockAPI) receiptFor(ctx context.Context, hash common.Hash) (*evmtypes.Receipt, error) { | ||
| stored, err := api.store.GetReceipt(receiptContext(ctx), hash) | ||
| func receiptFor(ctx context.Context, store receiptpkg.ReceiptStore, hash common.Hash) (*evmtypes.Receipt, error) { | ||
| stored, err := store.GetReceipt(receiptContext(ctx), hash) | ||
| if errors.Is(err, receiptpkg.ErrNotFound) { | ||
| return nil, nil | ||
| } | ||
|
|
@@ -197,3 +177,31 @@ func (api *blockAPI) receiptFor(ctx context.Context, hash common.Hash) (*evmtype | |
| } | ||
| return stored, nil | ||
| } | ||
|
|
||
| // blockGasUsed returns the cumulative gas from the last receipt belonging to | ||
| // block at its original index, or zero when none is available. | ||
| func blockGasUsed(ctx context.Context, store receiptpkg.ReceiptStore, block *coretypes.ResultBlock) (uint64, error) { | ||
| // A trailing stale transaction may have no receipt or retain one from another | ||
| // block. Walk backwards to the last transaction with a receipt for this block. | ||
| // The total covers this lane's block; superblocks merging lanes would need a | ||
| // combined total instead. | ||
| number := block.Block.Height | ||
| if store == nil || number > store.LatestVersion() || number < store.EarliestVersion() { | ||
| return 0, nil | ||
| } | ||
| txs := block.Block.Txs | ||
| for i := len(txs) - 1; i >= 0; i-- { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The Smaller and independent: in the |
||
| tx, err := decodeBlockTx(txs[i], number, i) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| stored, err := receiptFor(ctx, store, tx.Hash()) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("read transaction receipt at block %d index %d: %w", number, i, err) | ||
| } | ||
| if stored != nil && stored.BlockNumber == uint64(number) && uint64(stored.TransactionIndex) == uint64(i) { //nolint:gosec // G115: height and index are non-negative. | ||
| return stored.CumulativeGasUsed, nil | ||
| } | ||
| } | ||
| return 0, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| package rpc | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "math/big" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/common/hexutil" | ||
| ethrpc "github.com/ethereum/go-ethereum/rpc" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/sei-protocol/sei-chain/giga/evmonly" | ||
| sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" | ||
| "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" | ||
| tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" | ||
| evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" | ||
| ) | ||
|
|
||
| func TestBlockGasRPCsUseLastReceiptAtMatchingHeight(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| first bool | ||
| lastHeight uint64 | ||
| replayFirst bool | ||
| want uint64 | ||
| }{ | ||
| {name: "last receipt matches", first: true, lastHeight: 9, want: 43_500}, | ||
| {name: "trailing replay of first transaction", first: true, lastHeight: 9, replayFirst: true, want: 43_500}, | ||
| {name: "trailing receipt missing", first: true, want: 21_000}, | ||
| {name: "trailing receipt from earlier block", first: true, lastHeight: 8, want: 21_000}, | ||
| {name: "trailing receipt from later block", first: true, lastHeight: 10, want: 21_000}, | ||
| {name: "no matching receipts", lastHeight: 8}, | ||
| {name: "all receipts missing"}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| tx1, raw1 := testSignedTransaction(t) | ||
| tx2, raw2 := secondSignedTransaction(t) | ||
| store := evmonly.NewMemoryReceiptStore() | ||
| var records []receipt.ReceiptRecord | ||
| if tc.first { | ||
| records = append(records, receipt.ReceiptRecord{TxHash: tx1.Hash(), Receipt: &evmtypes.Receipt{ | ||
| TxHashHex: tx1.Hash().Hex(), BlockNumber: 9, CumulativeGasUsed: 21_000, | ||
| }}) | ||
| } | ||
| if tc.lastHeight != 0 { | ||
| records = append(records, receipt.ReceiptRecord{TxHash: tx2.Hash(), Receipt: &evmtypes.Receipt{ | ||
| TxHashHex: tx2.Hash().Hex(), BlockNumber: tc.lastHeight, TransactionIndex: 1, CumulativeGasUsed: 43_500, | ||
| }}) | ||
| } | ||
| require.NoError(t, store.SetReceipts(sdk.Context{}.WithContext(t.Context()).WithBlockHeight(9), records)) | ||
| trailing := raw2 | ||
| if tc.replayFirst { | ||
| trailing = raw1 | ||
| } | ||
| blockHash := common.HexToHash("0x9") | ||
| block := &coretypes.ResultBlock{ | ||
| BlockID: tmtypes.BlockID{Hash: blockHash.Bytes()}, | ||
| Block: &tmtypes.Block{ | ||
| Header: tmtypes.Header{Height: 9, Time: time.Unix(1_700_000_000, 0)}, | ||
| Data: tmtypes.Data{Txs: tmtypes.Txs{raw1, raw2, trailing}}, | ||
| }, | ||
| } | ||
| backend := fixedGasLimitBackend(t, 100_000, func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { | ||
| return block, nil | ||
| }) | ||
| backend.blockByHash = func(context.Context, *coretypes.RequestBlockByHash) (*coretypes.ResultBlock, error) { | ||
| return block, nil | ||
| } | ||
| backend.minGasPrice = func() (*big.Int, error) { return big.NewInt(1_000_000_000), nil } | ||
| blocks := &blockAPI{backend: backend, store: store} | ||
| for _, fullTx := range []bool{false, true} { | ||
| byNumber, err := blocks.GetBlockByNumber(t.Context(), 9, fullTx) | ||
| require.NoError(t, err) | ||
| require.Equal(t, hexutil.Uint64(tc.want), byNumber["gasUsed"]) | ||
| byHash, err := blocks.GetBlockByHash(t.Context(), blockHash, fullTx) | ||
| require.NoError(t, err) | ||
| require.Equal(t, hexutil.Uint64(tc.want), byHash["gasUsed"]) | ||
| } | ||
| history, err := (&infoAPI{backend: backend, store: store}).FeeHistory(t.Context(), 1, ethrpc.BlockNumber(9), nil) | ||
| require.NoError(t, err) | ||
| require.Equal(t, []float64{float64(tc.want) / 100_000}, history.GasUsedRatio) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| type failingGasReceiptStore struct { | ||
| receipt.ReceiptStore | ||
| hash common.Hash | ||
| err error | ||
| } | ||
|
|
||
| func (s failingGasReceiptStore) GetReceipt(ctx sdk.Context, hash common.Hash) (*evmtypes.Receipt, error) { | ||
| if hash == s.hash { | ||
| return nil, s.err | ||
| } | ||
| return s.ReceiptStore.GetReceipt(ctx, hash) | ||
| } | ||
|
|
||
| func TestBlockGasRPCsPropagateReceiptReadErrors(t *testing.T) { | ||
| tx, raw := testSignedTransaction(t) | ||
| _, trailing := secondSignedTransaction(t) | ||
| readErr := errors.New("receipt read failed") | ||
| memory := evmonly.NewMemoryReceiptStore() | ||
| require.NoError(t, memory.SetLatestVersion(9)) | ||
| store := failingGasReceiptStore{ReceiptStore: memory, hash: tx.Hash(), err: readErr} | ||
| block := &coretypes.ResultBlock{Block: &tmtypes.Block{ | ||
| Header: tmtypes.Header{Height: 9, Time: time.Unix(1_700_000_000, 0)}, | ||
| Data: tmtypes.Data{Txs: tmtypes.Txs{raw, trailing}}, | ||
| }} | ||
| backend := fixedGasLimitBackend(t, 100_000, func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { | ||
| return block, nil | ||
| }) | ||
| backend.minGasPrice = func() (*big.Int, error) { return big.NewInt(1_000_000_000), nil } | ||
| for _, fullTx := range []bool{false, true} { | ||
| _, err := (&blockAPI{backend: backend, store: store}).GetBlockByNumber(t.Context(), 9, fullTx) | ||
| require.ErrorIs(t, err, readErr) | ||
| } | ||
| _, err := (&infoAPI{backend: backend, store: store}).FeeHistory(t.Context(), 1, 9, nil) | ||
| require.ErrorIs(t, err, readErr) | ||
| } | ||
|
|
||
| func TestBlockGasRPCsSkipUnavailableReceiptHeights(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| latest int64 | ||
| earliest int64 | ||
| }{ | ||
| {name: "pending", latest: 8}, | ||
| {name: "pruned", latest: 10, earliest: 10}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| memory := evmonly.NewMemoryReceiptStore() | ||
| require.NoError(t, memory.SetLatestVersion(tc.latest)) | ||
| require.NoError(t, memory.SetEarliestVersion(tc.earliest)) | ||
| store := &countingReceiptStore{ReceiptStore: memory} | ||
| _, raw := testSignedTransaction(t) | ||
| block := &coretypes.ResultBlock{Block: &tmtypes.Block{ | ||
| Header: tmtypes.Header{Height: 9, Time: time.Unix(1_700_000_000, 0)}, | ||
| Data: tmtypes.Data{Txs: make(tmtypes.Txs, 100)}, | ||
| }} | ||
| for i := range block.Block.Txs { | ||
| block.Block.Txs[i] = raw | ||
| } | ||
| backend := fixedGasLimitBackend(t, 100_000, func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { | ||
| return block, nil | ||
| }) | ||
| backend.minGasPrice = func() (*big.Int, error) { return big.NewInt(1_000_000_000), nil } | ||
| got, err := (&blockAPI{backend: backend, store: store}).GetBlockByNumber(t.Context(), 9, false) | ||
| require.NoError(t, err) | ||
| require.Equal(t, hexutil.Uint64(0), got["gasUsed"]) | ||
| history, err := (&infoAPI{backend: backend, store: store}).FeeHistory(t.Context(), 1, 9, nil) | ||
| require.NoError(t, err) | ||
| require.Equal(t, []float64{0}, history.GasUsedRatio) | ||
| require.Zero(t, store.reads) | ||
| // Gas lookup also skips decoding transactions for unavailable heights. | ||
| block.Block.Txs[99] = []byte("invalid transaction") | ||
| gas, err := blockGasUsed(t.Context(), store, block) | ||
| require.NoError(t, err) | ||
| require.Zero(t, gas) | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.