Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions giga/evmonly/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,29 +1268,6 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) {
require.Equal(t, big.NewInt(0), state.GetBalance(recipient))
})

t.Run("nonce too low", func(t *testing.T) {
key, err := crypto.GenerateKey()
require.NoError(t, err)
sender := crypto.PubkeyToAddress(key.PublicKey)

state := NewMemoryState()
state.SetBalance(sender, big.NewInt(1_000_000_000_000_000))
state.SetNonce(sender, 1)
rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil)
executor := NewExecutor(Config{}, withTestState(state))

result, err := executor.ExecuteBlock(t.Context(), BlockRequest{
Context: blockContext(chainID),
Txs: [][]byte{rawTx},
})

require.Error(t, err)
require.True(t, errors.Is(err, core.ErrNonceTooLow))
require.Nil(t, result)
require.Equal(t, uint64(1), state.GetNonce(sender))
require.Equal(t, big.NewInt(0), state.GetBalance(recipient))
})

t.Run("insufficient balance", func(t *testing.T) {
key, err := crypto.GenerateKey()
require.NoError(t, err)
Expand Down
7 changes: 6 additions & 1 deletion giga/evmonly/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package evmonly

import (
"context"
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"

sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt"
Expand Down Expand Up @@ -67,7 +69,10 @@ func encodeReceiptRecord(blockNumber uint64, result *BlockResult, i int, records
if txResult.Err != nil {
stored.VmError = txResult.Err.Error()
}
records[i] = receipt.ReceiptRecord{TxHash: ethReceipt.TxHash, Receipt: stored}
records[i] = receipt.ReceiptRecord{
TxHash: ethReceipt.TxHash, Receipt: stored,
KeepExisting: errors.Is(txResult.Err, core.ErrNonceTooLow),
}
}
return nil
}
Expand Down
12 changes: 10 additions & 2 deletions giga/evmonly/receipt_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ func (s *MemoryReceiptStore) SetReceipts(ctx sdk.Context, records []receipt.Rece
latestVersion = blockVersion
}
stored = append(stored, receipt.ReceiptRecord{
TxHash: record.TxHash,
Receipt: cloneStoredReceipt(record.Receipt),
TxHash: record.TxHash,
Receipt: cloneStoredReceipt(record.Receipt),
KeepExisting: record.KeepExisting,
})
}
if err := receiptContextError(ctx); err != nil {
Expand All @@ -148,6 +149,13 @@ func (s *MemoryReceiptStore) storeRecords(ctx sdk.Context, stored []receipt.Rece
if err := receiptContextError(ctx); err != nil {
return err
}
stored, err := receipt.FilterExistingReceipts(stored, func(hash common.Hash) (bool, error) {
_, found := s.byTxHash[hash]
return found, nil
})
if err != nil {
return err
}
for _, record := range stored {
if previous, ok := s.byTxHash[record.TxHash]; ok {
delete(s.blocks[previous.blockNumber], record.TxHash)
Expand Down
66 changes: 37 additions & 29 deletions giga/evmonly/rpc/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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{
Expand All @@ -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),
Expand All @@ -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
}
Expand All @@ -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-- {
Comment thread
seidroid[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The LatestVersion/EarliestVersion guard closes the pending and pruned cases, but for a retained, available block this scan is still one receipt read plus one UnmarshalBinary per trailing transaction that does not match. A block that re-submits the previous block's transaction set makes every one of its receipts fail the BlockNumber == number test, so the scan walks the whole block; a stale transaction burns no gas, so the transaction count there is bounded by consensus MaxBytes rather than gasLimit / 21000. eth_feeHistory multiplies that by up to maxFeeHistoryBlockCount (1024, info.go:19) in a single public RPC call. The PR description scopes this explicitly, so it is a tradeoff rather than an oversight — recording it here because it is the one remaining amplification factor, and because carrying the block's gas total alongside the cursor changeset the app already persists would make it O(1) and independent of mutable hash lookups.

Smaller and independent: in the fullTx path encodeBlock has already read and decoded every transaction's receipt by the time it calls blockGasUsed, which then re-reads and re-decodes from the end. Threading the already-loaded receipts through would drop that duplicate work.

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
}
166 changes: 166 additions & 0 deletions giga/evmonly/rpc/block_gas_test.go
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)
})
}
}
Loading
Loading