From dc3794e3dc23fc8df9f202a7df7fde0b4604e951 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:05:31 -0500 Subject: [PATCH 01/27] core/rawdb: BAL storage layer (#34064) Add persistent storage for Block Access Lists (BALs) in `core/rawdb/`. This provides read/write/delete accessors for BALs in the active key-value store. --------- Co-authored-by: Jared Wasinger Co-authored-by: Gary Rong --- consensus/beacon/consensus.go | 20 +++++-- core/rawdb/accessors_chain.go | 66 +++++++++++++++++++- core/rawdb/accessors_chain_test.go | 77 ++++++++++++++++++++++++ core/rawdb/database.go | 5 ++ core/rawdb/schema.go | 6 ++ core/types/bal/bal_encoding.go | 11 ++-- core/types/bal/bal_test.go | 4 +- core/types/block.go | 30 +++++++++- core/types/gen_header_json.go | 96 ++++++++++++++++-------------- core/types/gen_header_rlp.go | 24 +++++--- 10 files changed, 270 insertions(+), 69 deletions(-) diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 25f4f9d2b2..c4a284d485 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -275,12 +275,22 @@ func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, pa } } + // Verify the existence / non-existence of Amsterdam-specific header fields amsterdam := chain.Config().IsAmsterdam(header.Number, header.Time) - if amsterdam && header.SlotNumber == nil { - return errors.New("header is missing slotNumber") - } - if !amsterdam && header.SlotNumber != nil { - return fmt.Errorf("invalid slotNumber: have %d, expected nil", *header.SlotNumber) + if amsterdam { + if header.BlockAccessListHash == nil { + return errors.New("header is missing block access list hash") + } + if header.SlotNumber == nil { + return errors.New("header is missing slotNumber") + } + } else { + if header.BlockAccessListHash != nil { + return fmt.Errorf("invalid block access list hash: have %x, expected nil", *header.BlockAccessListHash) + } + if header.SlotNumber != nil { + return fmt.Errorf("invalid slotNumber: have %d, expected nil", *header.SlotNumber) + } } return nil } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 6ae64fb2fd..0582e842c3 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -605,6 +606,55 @@ func DeleteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { } } +// HasAccessList verifies the existence of a block access list for a block. +func HasAccessList(db ethdb.Reader, hash common.Hash, number uint64) bool { + has, _ := db.Has(accessListKey(number, hash)) + return has +} + +// ReadAccessListRLP retrieves the RLP-encoded block access list for a block from KV. +func ReadAccessListRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Get(accessListKey(number, hash)) + return data +} + +// ReadAccessList retrieves and decodes the block access list for a block. +func ReadAccessList(db ethdb.Reader, hash common.Hash, number uint64) *bal.BlockAccessList { + data := ReadAccessListRLP(db, hash, number) + if len(data) == 0 { + return nil + } + b := new(bal.BlockAccessList) + if err := rlp.DecodeBytes(data, b); err != nil { + log.Error("Invalid BAL RLP", "hash", hash, "err", err) + return nil + } + return b +} + +// WriteAccessList RLP-encodes and stores a block access list in the active KV store. +func WriteAccessList(db ethdb.KeyValueWriter, hash common.Hash, number uint64, b *bal.BlockAccessList) { + bytes, err := rlp.EncodeToBytes(b) + if err != nil { + log.Crit("Failed to encode BAL", "err", err) + } + WriteAccessListRLP(db, hash, number, bytes) +} + +// WriteAccessListRLP stores a pre-encoded block access list in the active KV store. +func WriteAccessListRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, encoded rlp.RawValue) { + if err := db.Put(accessListKey(number, hash), encoded); err != nil { + log.Crit("Failed to store BAL", "err", err) + } +} + +// DeleteAccessList removes a block access list from the active KV store. +func DeleteAccessList(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { + if err := db.Delete(accessListKey(number, hash)); err != nil { + log.Crit("Failed to delete BAL", "err", err) + } +} + // ReceiptLogs is a barebone version of ReceiptForStorage which only keeps // the list of logs. When decoding a stored receipt into this object we // avoid creating the bloom filter. @@ -659,13 +709,25 @@ func ReadBlock(db ethdb.Reader, hash common.Hash, number uint64) *types.Block { if body == nil { return nil } - return types.NewBlockWithHeader(header).WithBody(*body) + block := types.NewBlockWithHeader(header).WithBody(*body) + + // Best-effort assembly of the block access list from the database. + if header.BlockAccessListHash != nil { + al := ReadAccessList(db, hash, number) + block = block.WithAccessListUnsafe(al) + } + return block } // WriteBlock serializes a block into the database, header and body separately. func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) { - WriteBody(db, block.Hash(), block.NumberU64(), block.Body()) + hash, number := block.Hash(), block.NumberU64() + WriteBody(db, hash, number, block.Body()) WriteHeader(db, block.Header()) + + if accessList := block.AccessList(); accessList != nil { + WriteAccessList(db, hash, number, accessList) + } } // WriteAncientBlocks writes entire block data into ancient store and returns the total written size. diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index 280fc21e8f..c35f56ee07 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -27,10 +27,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" ) // Tests block header storage and retrieval operations. @@ -899,3 +901,78 @@ func TestHeadersRLPStorage(t *testing.T) { checkSequence(1, 1) // Only block 1 checkSequence(1, 2) // Genesis + block 1 } + +func makeTestBAL(t *testing.T) (rlp.RawValue, *bal.BlockAccessList) { + t.Helper() + + cb := bal.NewConstructionBlockAccessList() + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + cb.AccountRead(addr) + cb.StorageRead(addr, common.BytesToHash([]byte{0x01})) + cb.StorageWrite(0, addr, common.BytesToHash([]byte{0x02}), common.BytesToHash([]byte{0xaa})) + cb.BalanceChange(0, addr, uint256.NewInt(100)) + cb.NonceChange(addr, 0, 1) + + var buf bytes.Buffer + if err := cb.EncodeRLP(&buf); err != nil { + t.Fatalf("failed to encode BAL: %v", err) + } + encoded := buf.Bytes() + + var decoded bal.BlockAccessList + if err := rlp.DecodeBytes(encoded, &decoded); err != nil { + t.Fatalf("failed to decode BAL: %v", err) + } + return encoded, &decoded +} + +// TestBALStorage tests write/read/delete of BALs in the KV store. +func TestBALStorage(t *testing.T) { + db := NewMemoryDatabase() + + hash := common.BytesToHash([]byte{0x03, 0x14}) + number := uint64(42) + + // Check that no BAL exists in a new database. + if HasAccessList(db, hash, number) { + t.Fatal("BAL found in new database") + } + if b := ReadAccessList(db, hash, number); b != nil { + t.Fatalf("non existent BAL returned: %v", b) + } + + // Write a BAL and verify it can be read back. + encoded, testBAL := makeTestBAL(t) + WriteAccessList(db, hash, number, testBAL) + + if !HasAccessList(db, hash, number) { + t.Fatal("HasAccessList returned false after write") + } + if blob := ReadAccessListRLP(db, hash, number); len(blob) == 0 { + t.Fatal("ReadAccessListRLP returned empty after write") + } + if b := ReadAccessList(db, hash, number); b == nil { + t.Fatal("ReadAccessList returned nil after write") + } else if b.Hash() != testBAL.Hash() { + t.Fatalf("BAL hash mismatch: got %x, want %x", b.Hash(), testBAL.Hash()) + } + + // Also test WriteAccessListRLP with pre-encoded data. + hash2 := common.BytesToHash([]byte{0x03, 0x15}) + WriteAccessListRLP(db, hash2, number, encoded) + if b := ReadAccessList(db, hash2, number); b == nil { + t.Fatal("ReadAccessList returned nil after WriteAccessListRLP") + } else if b.Hash() != testBAL.Hash() { + t.Fatalf("BAL hash mismatch after WriteAccessListRLP: got %x, want %x", b.Hash(), testBAL.Hash()) + } + + // Delete the BAL and verify it's gone. + DeleteAccessList(db, hash, number) + + if HasAccessList(db, hash, number) { + t.Fatal("HasAccessList returned true after delete") + } + if b := ReadAccessList(db, hash, number); b != nil { + t.Fatalf("deleted BAL returned: %v", b) + } +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 945fd9097d..39e1a64e5a 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -413,6 +413,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { tds stat numHashPairings stat hashNumPairings stat + blockAccessList stat legacyTries stat stateLookups stat accountTries stat @@ -484,6 +485,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { numHashPairings.add(size) case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): hashNumPairings.add(size) + case bytes.HasPrefix(key, accessListPrefix) && len(key) == len(accessListPrefix)+8+common.HashLength: + blockAccessList.add(size) + case IsLegacyTrieNode(key, it.Value()): legacyTries.add(size) case bytes.HasPrefix(key, stateIDPrefix) && len(key) == len(stateIDPrefix)+common.HashLength: @@ -625,6 +629,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"Key-Value store", "Difficulties (deprecated)", tds.sizeString(), tds.countString()}, {"Key-Value store", "Block number->hash", numHashPairings.sizeString(), numHashPairings.countString()}, {"Key-Value store", "Block hash->number", hashNumPairings.sizeString(), hashNumPairings.countString()}, + {"Key-Value store", "Block accessList", blockAccessList.sizeString(), blockAccessList.countString()}, {"Key-Value store", "Transaction index", txLookups.sizeString(), txLookups.countString()}, {"Key-Value store", "Log index filter-map rows", filterMapRows.sizeString(), filterMapRows.countString()}, {"Key-Value store", "Log index last-block-of-map", filterMapLastBlock.sizeString(), filterMapLastBlock.countString()}, diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index d9140c5fd6..54c76143b4 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -112,6 +112,7 @@ var ( blockBodyPrefix = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts + accessListPrefix = []byte("j") // accessListPrefix + num (uint64 big endian) + hash -> block access list txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits @@ -214,6 +215,11 @@ func blockReceiptsKey(number uint64, hash common.Hash) []byte { return append(append(blockReceiptsPrefix, encodeBlockNumber(number)...), hash.Bytes()...) } +// accessListKey = accessListPrefix + num (uint64 big endian) + hash +func accessListKey(number uint64, hash common.Hash) []byte { + return append(append(accessListPrefix, encodeBlockNumber(number)...), hash.Bytes()...) +} + // txLookupKey = txLookupPrefix + hash func txLookupKey(hash common.Hash) []byte { return append(txLookupPrefix, hash.Bytes()...) diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 1b1406ea32..6d52c17c83 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -350,9 +350,12 @@ func (e *BlockAccessList) PrettyPrint() string { } // Copy returns a deep copy of the access list -func (e *BlockAccessList) Copy() (res BlockAccessList) { - for _, accountAccess := range e.Accesses { - res.Accesses = append(res.Accesses, accountAccess.Copy()) +func (e *BlockAccessList) Copy() *BlockAccessList { + cpy := &BlockAccessList{ + Accesses: make([]AccountAccess, 0, len(e.Accesses)), } - return + for _, accountAccess := range e.Accesses { + cpy.Accesses = append(cpy.Accesses, accountAccess.Copy()) + } + return cpy } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 52c0de825e..58ba639ff0 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -190,8 +190,8 @@ func makeTestAccountAccess(sort bool) AccountAccess { } } -func makeTestBAL(sort bool) BlockAccessList { - list := BlockAccessList{} +func makeTestBAL(sort bool) *BlockAccessList { + list := &BlockAccessList{} for i := 0; i < 5; i++ { list.Accesses = append(list.Accesses, makeTestAccountAccess(sort)) } diff --git a/core/types/block.go b/core/types/block.go index d092351b58..2d65adeff3 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/rlp" ) @@ -99,6 +100,9 @@ type Header struct { // RequestsHash was added by EIP-7685 and is ignored in legacy headers. RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + // BlockAccessListHash was added by EIP-7928 and is ignored in legacy headers. + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + // SlotNumber was added by EIP-7843 and is ignored in legacy headers. SlotNumber *uint64 `json:"slotNumber" rlp:"optional"` } @@ -204,6 +208,7 @@ type Block struct { uncles []*Header transactions Transactions withdrawals Withdrawals + accessList *bal.BlockAccessList // caches hash atomic.Pointer[common.Hash] @@ -358,9 +363,10 @@ func (b *Block) Body() *Body { // Accessors for body data. These do not return a copy because the content // of the body slices does not affect the cached hash/size in block. -func (b *Block) Uncles() []*Header { return b.uncles } -func (b *Block) Transactions() Transactions { return b.transactions } -func (b *Block) Withdrawals() Withdrawals { return b.withdrawals } +func (b *Block) Uncles() []*Header { return b.uncles } +func (b *Block) Transactions() Transactions { return b.transactions } +func (b *Block) Withdrawals() Withdrawals { return b.withdrawals } +func (b *Block) AccessList() *bal.BlockAccessList { return b.accessList } func (b *Block) Transaction(hash common.Hash) *Transaction { for _, transaction := range b.transactions { @@ -513,6 +519,24 @@ func (b *Block) WithBody(body Body) *Block { return block } +// WithAccessList returns a copy of the block with the given access list embedded. +func (b *Block) WithAccessList(accessList *bal.BlockAccessList) *Block { + return b.WithAccessListUnsafe(accessList.Copy()) +} + +// WithAccessListUnsafe returns a copy of the block with the given access list +// embedded. Note that the access list is not deep-copied; use WithAccessList +// if the provided list may be modified by other actors. +func (b *Block) WithAccessListUnsafe(accessList *bal.BlockAccessList) *Block { + return &Block{ + header: b.header, + transactions: b.transactions, + uncles: b.uncles, + withdrawals: b.withdrawals, + accessList: accessList, + } +} + // Hash returns the keccak256 hash of b's header. // The hash is computed on the first call and cached thereafter. func (b *Block) Hash() common.Hash { diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 16fb03f612..2e2f1cdca5 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -16,29 +16,30 @@ var _ = (*headerMarshaling)(nil) // MarshalJSON marshals as JSON. func (h Header) MarshalJSON() ([]byte, error) { type Header struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase common.Address `json:"miner"` - Root common.Hash `json:"stateRoot" gencodec:"required"` - TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest common.Hash `json:"mixHash"` - Nonce BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` - Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` + Hash common.Hash `json:"hash"` } var enc Header enc.ParentHash = h.ParentHash @@ -62,6 +63,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot enc.RequestsHash = h.RequestsHash + enc.BlockAccessListHash = h.BlockAccessListHash enc.SlotNumber = (*hexutil.Uint64)(h.SlotNumber) enc.Hash = h.Hash() return json.Marshal(&enc) @@ -70,28 +72,29 @@ func (h Header) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (h *Header) UnmarshalJSON(input []byte) error { type Header struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase *common.Address `json:"miner"` - Root *common.Hash `json:"stateRoot" gencodec:"required"` - TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom *Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest *common.Hash `json:"mixHash"` - Nonce *BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -172,6 +175,9 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.RequestsHash != nil { h.RequestsHash = dec.RequestsHash } + if dec.BlockAccessListHash != nil { + h.BlockAccessListHash = dec.BlockAccessListHash + } if dec.SlotNumber != nil { h.SlotNumber = (*uint64)(dec.SlotNumber) } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index cfbd57ab8a..3b7eb2c926 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -43,8 +43,9 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { _tmp4 := obj.ExcessBlobGas != nil _tmp5 := obj.ParentBeaconRoot != nil _tmp6 := obj.RequestsHash != nil - _tmp7 := obj.SlotNumber != nil - if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { + _tmp7 := obj.BlockAccessListHash != nil + _tmp8 := obj.SlotNumber != nil + if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -54,42 +55,49 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } - if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { + if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 { if obj.WithdrawalsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.WithdrawalsHash[:]) } } - if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { + if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 { if obj.BlobGasUsed == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.BlobGasUsed)) } } - if _tmp4 || _tmp5 || _tmp6 || _tmp7 { + if _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 { if obj.ExcessBlobGas == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.ExcessBlobGas)) } } - if _tmp5 || _tmp6 || _tmp7 { + if _tmp5 || _tmp6 || _tmp7 || _tmp8 { if obj.ParentBeaconRoot == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.ParentBeaconRoot[:]) } } - if _tmp6 || _tmp7 { + if _tmp6 || _tmp7 || _tmp8 { if obj.RequestsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.RequestsHash[:]) } } - if _tmp7 { + if _tmp7 || _tmp8 { + if obj.BlockAccessListHash == nil { + w.Write([]byte{0x80}) + } else { + w.WriteBytes(obj.BlockAccessListHash[:]) + } + } + if _tmp8 { if obj.SlotNumber == nil { w.Write([]byte{0x80}) } else { From 3da517e239eafdb7839650bd8e4667eb85feb5ba Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:47:07 +0200 Subject: [PATCH 02/27] core/state: fix storage counters in binary trie IntermediateRoot (#34110) Add missing `StorageUpdated` and `StorageDeleted` counter increments in the binary trie fast path of `IntermediateRoot()`. --- core/state/statedb.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/state/statedb.go b/core/state/statedb.go index 93dd7d6488..769c8504c2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -879,10 +879,12 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil { s.setError(err) } + s.StorageUpdated.Add(1) } else { if err := s.trie.DeleteStorage(addr, key[:]); err != nil { s.setError(err) } + s.StorageDeleted.Add(1) } } } From 92b4cb266354076fc9e8cb8b4c77fbac7a42cec4 Mon Sep 17 00:00:00 2001 From: Chase Wright Date: Tue, 31 Mar 2026 09:02:40 -0500 Subject: [PATCH 03/27] eth/tracers/logger: conform structLog tracing to spec (#34093) This is a breaking change in the opcode (structLog) tracer. Several fields will have a slight formatting difference to conform to the newly established spec at: https://github.com/ethereum/execution-apis/pull/762. The differences include: - `memory`: words will have the 0x prefix. Also last word of memory will be padded to 32-bytes. - `storage`: keys and values will have the 0x prefix. --------- Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com> --- eth/api_backend.go | 8 +- eth/api_debug.go | 2 +- eth/state_accessor.go | 26 +++-- eth/tracers/api.go | 50 ++------ eth/tracers/api_test.go | 188 +++++++++++++++++++++++++++++- eth/tracers/logger/logger.go | 17 ++- eth/tracers/logger/logger_test.go | 43 +++++++ 7 files changed, 269 insertions(+), 65 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 726d8316a0..fe2105f47b 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -484,12 +484,12 @@ func (b *EthAPIBackend) CurrentHeader() *types.Header { return b.eth.blockchain.CurrentHeader() } -func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtBlock(ctx, block, reexec, base, readOnly, preferDisk) +func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) { + return b.eth.stateAtBlock(ctx, block, base, readOnly, preferDisk) } -func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) +func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { + return b.eth.stateAtTransaction(ctx, block, txIndex) } func (b *EthAPIBackend) RPCTxSyncDefaultTimeout() time.Duration { diff --git a/eth/api_debug.go b/eth/api_debug.go index b8267902b2..988eb44216 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -222,7 +222,7 @@ func (api *DebugAPI) StorageRangeAt(ctx context.Context, blockNrOrHash rpc.Block if block == nil { return StorageRangeResult{}, fmt.Errorf("block %v not found", blockNrOrHash) } - _, _, statedb, release, err := api.eth.stateAtTransaction(ctx, block, txIndex, 0) + _, _, statedb, release, err := api.eth.stateAtTransaction(ctx, block, txIndex) if err != nil { return StorageRangeResult{}, err } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 871f2c9269..04aac321cb 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -38,7 +38,11 @@ import ( // for releasing state. var noopReleaser = tracers.StateReleaseFunc(func() {}) -func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { +// reexecLimit is the maximum number of ancestor blocks to walk back when +// attempting to reconstruct missing historical state for hash-scheme nodes. +const reexecLimit = uint64(128) + +func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { var ( current *types.Block database state.Database @@ -99,7 +103,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u } } // Database does not have the state for the given block, try to regenerate - for i := uint64(0); i < reexec; i++ { + for i := uint64(0); i < reexecLimit; i++ { if err := ctx.Err(); err != nil { return nil, nil, err } @@ -120,7 +124,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u if err != nil { switch err.(type) { case *trie.MissingNodeError: - return nil, nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexec) + return nil, nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexecLimit) default: return nil, nil, err } @@ -190,10 +194,9 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro } // stateAtBlock retrieves the state database associated with a certain block. -// If no state is locally available for the given block, a number of blocks -// are attempted to be reexecuted to generate the desired state. The optional -// base layer statedb can be provided which is regarded as the statedb of the -// parent block. +// If no state is locally available for the given block, up to reexecLimit ancestor +// blocks are reexecuted to generate the desired state. The optional base layer +// statedb can be provided which is regarded as the statedb of the parent block. // // An additional release function will be returned if the requested state is // available. Release is expected to be invoked when the returned state is no @@ -202,7 +205,6 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro // // Parameters: // - block: The block for which we want the state(state = block.Root) -// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state // - base: If the caller is tracing multiple blocks, the caller can provide the parent // state continuously from the callsite. // - readOnly: If true, then the live 'blockchain' state database is used. No mutation should @@ -211,9 +213,9 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro // - preferDisk: This arg can be used by the caller to signal that even though the 'base' is // provided, it would be preferable to start from a fresh state, if we have it // on disk. -func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { +func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme { - return eth.hashState(ctx, block, reexec, base, readOnly, preferDisk) + return eth.hashState(ctx, block, base, readOnly, preferDisk) } return eth.pathState(block) } @@ -225,7 +227,7 @@ func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexe // function will return the state of block after the pre-block operations have // been completed (e.g. updating system contracts), but before post-block // operations are completed (e.g. processing withdrawals). -func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { +func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { // Short circuit if it's genesis block. if block.NumberU64() == 0 { return nil, vm.BlockContext{}, nil, nil, errors.New("no transaction in genesis") @@ -237,7 +239,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, } // Lookup the statedb of parent block from the live database, // otherwise regenerate it on the flight. - statedb, release, err := eth.stateAtBlock(ctx, parent, reexec, nil, true, false) + statedb, release, err := eth.stateAtBlock(ctx, parent, nil, true, false) if err != nil { return nil, vm.BlockContext{}, nil, nil, err } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index eed404622e..53a09087e4 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -51,11 +51,6 @@ const ( // by default before being forcefully aborted. defaultTraceTimeout = 5 * time.Second - // defaultTraceReexec is the number of blocks the tracer is willing to go back - // and reexecute to produce missing historical state necessary to run a specific - // trace. - defaultTraceReexec = uint64(128) - // defaultTracechainMemLimit is the size of the triedb, at which traceChain // switches over and tries to use a disk-backed database instead of building // on top of memory. @@ -89,8 +84,8 @@ type Backend interface { ChainConfig() *params.ChainConfig Engine() consensus.Engine ChainDb() ethdb.Database - StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) - StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) + StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) + StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) } // API is the collection of tracing APIs exposed over the private debugging endpoint. @@ -156,7 +151,6 @@ type TraceConfig struct { *logger.Config Tracer *string Timeout *string - Reexec *uint64 // Config specific to given tracer. Note struct logger // config are historically embedded in main object. TracerConfig json.RawMessage @@ -174,7 +168,6 @@ type TraceCallConfig struct { // StdTraceConfig holds extra parameters to standard-json trace functions. type StdTraceConfig struct { logger.Config - Reexec *uint64 TxHash common.Hash } @@ -245,10 +238,6 @@ func (api *API) TraceChain(ctx context.Context, start, end rpc.BlockNumber, conf // transaction, dependent on the requested tracer. // The tracing procedure should be aborted in case the closed signal is received. func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed <-chan error) chan *blockTraceResult { - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } blocks := int(end.NumberU64() - start.NumberU64()) threads := runtime.NumCPU() if threads > blocks { @@ -374,7 +363,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed s1, s2, s3 := statedb.Database().TrieDB().Size() preferDisk = s1+s2+s3 > defaultTracechainMemLimit } - statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, statedb, false, preferDisk) + statedb, release, err = api.backend.StateAtBlock(ctx, block, statedb, false, preferDisk) if err != nil { failed = err break @@ -522,11 +511,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config if err != nil { return nil, err } - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } - statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) + statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false) if err != nil { return nil, err } @@ -591,11 +576,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac if err != nil { return nil, err } - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } - statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) + statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false) if err != nil { return nil, err } @@ -743,11 +724,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block if err != nil { return nil, err } - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } - statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) + statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false) if err != nil { return nil, err } @@ -877,15 +854,11 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config * if blockNumber == 0 { return nil, errors.New("genesis is not traceable") } - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } block, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(blockNumber), blockHash) if err != nil { return nil, err } - tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index), reexec) + tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index)) if err != nil { return nil, err } @@ -939,15 +912,10 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc return nil, err } // try to recompute the state - reexec := defaultTraceReexec - if config != nil && config.Reexec != nil { - reexec = *config.Reexec - } - if config != nil && config.TxIndex != nil { - _, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex), reexec) + _, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex)) } else { - statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, nil, true, false) + statedb, release, err = api.backend.StateAtBlock(ctx, block, nil, true, false) } if err != nil { return nil, err diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 1d5024ad08..ecf3c99c8f 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -151,7 +151,7 @@ func (b *testBackend) teardown() { b.chain.Stop() } -func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) { +func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) { statedb, err := b.chain.StateAt(block.Root()) if err != nil { return nil, nil, errStateNotFound @@ -167,12 +167,12 @@ func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reex return statedb, release, nil } -func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) { +func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) { parent := b.chain.GetBlock(block.ParentHash(), block.NumberU64()-1) if parent == nil { return nil, vm.BlockContext{}, nil, nil, errBlockNotFound } - statedb, release, err := b.StateAtBlock(ctx, parent, reexec, nil, true, false) + statedb, release, err := b.StateAtBlock(ctx, parent, nil, true, false) if err != nil { return nil, vm.BlockContext{}, nil, nil, errStateNotFound } @@ -202,6 +202,18 @@ type stateTracer struct { Storage map[common.Address]map[common.Hash]common.Hash } +type tracedOpcodeLog struct { + Op string `json:"op"` + Refund *uint64 `json:"refund,omitempty"` + Storage map[string]string `json:"storage,omitempty"` +} + +type tracedOpcodeResult struct { + Failed bool `json:"failed"` + ReturnValue string `json:"returnValue"` + StructLogs []tracedOpcodeLog `json:"structLogs"` +} + func newStateTracer(ctx *Context, cfg json.RawMessage, chainCfg *params.ChainConfig) (*Tracer, error) { t := &stateTracer{ Balance: make(map[common.Address]*hexutil.Big), @@ -1058,6 +1070,176 @@ func TestTracingWithOverrides(t *testing.T) { } } +func TestTraceTransactionRefundAndStorageSnapshots(t *testing.T) { + t.Parallel() + + accounts := newAccounts(1) + contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef") + slot0 := common.BigToHash(big.NewInt(0)) + txSigner := types.HomesteadSigner{} + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + contract: { + Nonce: 1, + Code: []byte{ + byte(vm.PUSH1), 0x00, + byte(vm.SLOAD), + byte(vm.POP), + byte(vm.PUSH1), 0x00, + byte(vm.PUSH1), 0x00, + byte(vm.SSTORE), + byte(vm.STOP), + }, + Storage: map[common.Hash]common.Hash{ + slot0: common.BigToHash(big.NewInt(1)), + }, + }, + }, + } + var target common.Hash + backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) { + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &contract, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: b.BaseFee(), + }), txSigner, accounts[0].key) + b.AddTx(tx) + target = tx.Hash() + }) + defer backend.teardown() + + api := NewAPI(backend) + result, err := api.TraceTransaction(context.Background(), target, nil) + if err != nil { + t.Fatalf("failed to trace refunding transaction: %v", err) + } + var traced tracedOpcodeResult + if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil { + t.Fatalf("failed to unmarshal trace result: %v", err) + } + if traced.Failed { + t.Fatal("expected refunding transaction to succeed") + } + if traced.ReturnValue != "0x" { + t.Fatalf("unexpected return value: have %s want 0x", traced.ReturnValue) + } + slotHex := slot0.Hex() + oneHex := common.BigToHash(big.NewInt(1)).Hex() + zeroHex := common.Hash{}.Hex() + var ( + foundSloadSnapshot bool + foundSstoreSnapshot bool + foundRefund bool + ) + for _, log := range traced.StructLogs { + switch log.Op { + case "SLOAD": + if got := log.Storage[slotHex]; got == oneHex { + foundSloadSnapshot = true + } + case "SSTORE": + if got := log.Storage[slotHex]; got == zeroHex { + foundSstoreSnapshot = true + } + } + if log.Refund != nil && *log.Refund > 0 { + foundRefund = true + } + } + if !foundSloadSnapshot { + t.Fatal("expected SLOAD snapshot to include the pre-existing non-zero storage value") + } + if !foundSstoreSnapshot { + t.Fatal("expected SSTORE snapshot to include the post-write zeroed storage value") + } + if !foundRefund { + t.Fatal("expected at least one structLog entry with a non-zero refund field") + } +} + +func TestTraceTransactionFailureReturnValues(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code []byte + wantReturnValue string + }{ + { + name: "revert preserves return data", + code: []byte{ + byte(vm.PUSH1), 0x2a, + byte(vm.PUSH1), 0x00, + byte(vm.MSTORE), + byte(vm.PUSH1), 0x20, + byte(vm.PUSH1), 0x00, + byte(vm.REVERT), + }, + wantReturnValue: "0x000000000000000000000000000000000000000000000000000000000000002a", + }, + { + name: "hard failure clears return data", + code: []byte{ + byte(vm.INVALID), + }, + wantReturnValue: "0x", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + accounts := newAccounts(1) + contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef") + txSigner := types.HomesteadSigner{} + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + contract: { + Nonce: 1, + Code: tc.code, + }, + }, + } + var target common.Hash + backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) { + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: 0, + To: &contract, + Value: big.NewInt(0), + Gas: 100000, + GasPrice: b.BaseFee(), + }), txSigner, accounts[0].key) + b.AddTx(tx) + target = tx.Hash() + }) + defer backend.teardown() + + api := NewAPI(backend) + result, err := api.TraceTransaction(context.Background(), target, nil) + if err != nil { + t.Fatalf("failed to trace transaction: %v", err) + } + var traced tracedOpcodeResult + if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil { + t.Fatalf("failed to unmarshal trace result: %v", err) + } + if !traced.Failed { + t.Fatal("expected traced transaction to fail") + } + if traced.ReturnValue != tc.wantReturnValue { + t.Fatalf("unexpected returnValue: have %s want %s", traced.ReturnValue, tc.wantReturnValue) + } + if len(traced.StructLogs) == 0 { + t.Fatal("expected failing trace to still include structLogs") + } + }) + } +} + type Account struct { key *ecdsa.PrivateKey addr common.Address diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 67e07f78d0..7f2b2aecf2 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -148,7 +148,7 @@ type structLogLegacy struct { Gas uint64 `json:"gas"` GasCost uint64 `json:"gasCost"` Depth int `json:"depth"` - Error string `json:"error,omitempty"` + Error string `json:"error,omitempty,omitzero"` Stack *[]string `json:"stack,omitempty"` ReturnData string `json:"returnData,omitempty"` Memory *[]string `json:"memory,omitempty"` @@ -156,6 +156,15 @@ type structLogLegacy struct { RefundCounter uint64 `json:"refund,omitempty"` } +func formatMemoryWord(chunk []byte) string { + if len(chunk) == 32 { + return hexutil.Encode(chunk) + } + var word [32]byte + copy(word[:], chunk) + return hexutil.Encode(word[:]) +} + // toLegacyJSON converts the structLog to legacy json-encoded legacy form. func (s *StructLog) toLegacyJSON() json.RawMessage { msg := structLogLegacy{ @@ -175,7 +184,7 @@ func (s *StructLog) toLegacyJSON() json.RawMessage { msg.Stack = &stack } if len(s.ReturnData) > 0 { - msg.ReturnData = hexutil.Bytes(s.ReturnData).String() + msg.ReturnData = hexutil.Encode(s.ReturnData) } if len(s.Memory) > 0 { memory := make([]string, 0, (len(s.Memory)+31)/32) @@ -184,14 +193,14 @@ func (s *StructLog) toLegacyJSON() json.RawMessage { if end > len(s.Memory) { end = len(s.Memory) } - memory = append(memory, fmt.Sprintf("%x", s.Memory[i:end])) + memory = append(memory, formatMemoryWord(s.Memory[i:end])) } msg.Memory = &memory } if len(s.Storage) > 0 { storage := make(map[string]string) for i, storageValue := range s.Storage { - storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) + storage[i.Hex()] = storageValue.Hex() } msg.Storage = &storage } diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index acc3069e70..554a37aff1 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -96,3 +96,46 @@ func TestStructLogMarshalingOmitEmpty(t *testing.T) { }) } } + +func TestStructLogLegacyJSONSpecFormatting(t *testing.T) { + tests := []struct { + name string + log *StructLog + want string + }{ + { + name: "omits empty error and pads memory/storage", + log: &StructLog{ + Pc: 7, + Op: vm.SSTORE, + Gas: 100, + GasCost: 20, + Memory: []byte{0xaa, 0xbb}, + Storage: map[common.Hash]common.Hash{common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(2))}, + Depth: 1, + ReturnData: []byte{0x12, 0x34}, + }, + want: `{"pc":7,"op":"SSTORE","gas":100,"gasCost":20,"depth":1,"returnData":"0x1234","memory":["0xaabb000000000000000000000000000000000000000000000000000000000000"],"storage":{"0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000002"}}`, + }, + { + name: "includes error only when present", + log: &StructLog{ + Pc: 1, + Op: vm.STOP, + Gas: 2, + GasCost: 3, + Depth: 1, + Err: errors.New("boom"), + }, + want: `{"pc":1,"op":"STOP","gas":2,"gasCost":3,"depth":1,"error":"boom"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + have := string(tt.log.toLegacyJSON()) + if have != tt.want { + t.Fatalf("mismatched results\n\thave: %v\n\twant: %v", have, tt.want) + } + }) + } +} From fc43170cdd2a72bfb1e99e72f4e836a2e8036501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Wed, 1 Apr 2026 16:05:57 +0200 Subject: [PATCH 04/27] beacon/light: keep retrying checkpoint init if failed (#33966) This PR changes the blsync checkpoint init logic so that even if the initialization fails with a certain server and an error log message is printed, the server goes back to its initial state and is allowed to retry initialization after the failure delay period. The previous logic had an `ssDone` server state that did put the server in a permanently unusable state once the checkpoint init failed for an apparently permanent reason. This was not the correct behavior because different servers behave differently in case of overload and sometimes the response to a permanently missing item is not clearly distinguishable from an overload response. A safer logic is to never assume anything to be permanent and always give a chance to retry. The failure delay formula is also fixed; now it is properly capped at `maxFailureDelay`. The previous formula did allow the delay to grow unlimited if a retry was attempted immediately after each delay period. --- beacon/light/request/server.go | 5 +---- beacon/light/sync/update_sync.go | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/beacon/light/request/server.go b/beacon/light/request/server.go index a06dec99ae..d39570b8e5 100644 --- a/beacon/light/request/server.go +++ b/beacon/light/request/server.go @@ -438,14 +438,11 @@ func (s *serverWithLimits) fail(desc string) { // failLocked calculates the dynamic failure delay and applies it. func (s *serverWithLimits) failLocked(desc string) { log.Debug("Server error", "description", desc) - s.failureDelay *= 2 now := s.clock.Now() if now > s.failureDelayEnd { s.failureDelay *= math.Pow(2, -float64(now-s.failureDelayEnd)/float64(maxFailureDelay)) } - if s.failureDelay < float64(minFailureDelay) { - s.failureDelay = float64(minFailureDelay) - } + s.failureDelay = max(min(s.failureDelay*2, float64(maxFailureDelay)), float64(minFailureDelay)) s.failureDelayEnd = now + mclock.AbsTime(s.failureDelay) s.delay(time.Duration(s.failureDelay)) } diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go index 9549ee5992..d84a3d64da 100644 --- a/beacon/light/sync/update_sync.go +++ b/beacon/light/sync/update_sync.go @@ -62,7 +62,6 @@ const ( ssNeedParent // cp header slot %32 != 0, need parent to check epoch boundary ssParentRequested // cp parent header requested ssPrintStatus // has all necessary info, print log message if init still not successful - ssDone // log message printed, no more action required ) type serverState struct { @@ -180,7 +179,8 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E default: log.Error("blsync: checkpoint not available, but reported as finalized; specified checkpoint hash might be too old", "server", server.Name()) } - s.serverState[server] = serverState{state: ssDone} + s.serverState[server] = serverState{state: ssDefault} + requester.Fail(server, "checkpoint init failed") } } From 14a26d9cccb3761b6a2034c30b831c06acb1694f Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Thu, 2 Apr 2026 02:32:17 +0800 Subject: [PATCH 05/27] eth/gasestimator: fix block overrides in estimate gas (#34081) Block overrides were to a great extent ignored by the gasestimator. This PR fixes that. --- eth/gasestimator/gasestimator.go | 29 +++++++++-------------------- internal/ethapi/api.go | 17 +++++++++++------ internal/ethapi/api_test.go | 28 ++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index 80aeb3d3b2..ad6491fd93 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -27,7 +27,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/internal/ethapi/override" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" ) @@ -38,11 +37,12 @@ import ( // these together, it would be excessively hard to test. Splitting the parts out // allows testing without needing a proper live chain. type Options struct { - Config *params.ChainConfig // Chain configuration for hard fork selection - Chain core.ChainContext // Chain context to access past block hashes - Header *types.Header // Header defining the block context to execute in - State *state.StateDB // Pre-state on top of which to estimate the gas - BlockOverrides *override.BlockOverrides // Block overrides to apply during the estimation + Config *params.ChainConfig // Chain configuration for hard fork selection + Chain core.ChainContext // Chain context to access past block hashes + Header *types.Header // Header defining the block context to execute in + State *state.StateDB // Pre-state on top of which to estimate the gas + + BlobBaseFee *big.Int // BlobBaseFee optionally overrides the blob base fee in the execution context. ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination } @@ -64,16 +64,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // Cap the maximum gas allowance according to EIP-7825 if the estimation targets Osaka if hi > params.MaxTxGas { - blockNumber, blockTime := opts.Header.Number, opts.Header.Time - if opts.BlockOverrides != nil { - if opts.BlockOverrides.Number != nil { - blockNumber = opts.BlockOverrides.Number.ToInt() - } - if opts.BlockOverrides.Time != nil { - blockTime = uint64(*opts.BlockOverrides.Time) - } - } - if opts.Config.IsOsaka(blockNumber, blockTime) { + if opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time) { hi = params.MaxTxGas } } @@ -241,10 +232,8 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil) dirtyState = opts.State.Copy() ) - if opts.BlockOverrides != nil { - if err := opts.BlockOverrides.Apply(&evmContext); err != nil { - return nil, err - } + if opts.BlobBaseFee != nil { + evmContext.BlobBaseFee = new(big.Int).Set(opts.BlobBaseFee) } // Lower the basefee to 0 to avoid breaking EVM // invariants (basefee < feecap). diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4f217d0578..694fbf9c15 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -897,6 +897,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr if err := blockOverrides.Apply(&blockCtx); err != nil { return 0, err } + header = blockOverrides.MakeHeader(header) } rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time) precompiles := vm.ActivePrecompiledContracts(rules) @@ -904,13 +905,17 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr return 0, err } // Construct the gas estimator option from the user input + var blobBaseFee *big.Int + if blockOverrides != nil && blockOverrides.BlobBaseFee != nil { + blobBaseFee = blockOverrides.BlobBaseFee.ToInt() + } opts := &gasestimator.Options{ - Config: b.ChainConfig(), - Chain: NewChainContext(ctx, b), - Header: header, - BlockOverrides: blockOverrides, - State: state, - ErrorRatio: estimateGasErrorRatio, + Config: b.ChainConfig(), + Chain: NewChainContext(ctx, b), + Header: header, + State: state, + BlobBaseFee: blobBaseFee, + ErrorRatio: estimateGasErrorRatio, } // Set any required transaction default, but make sure the gas cap itself is not messed with // if it was not specified in the original argument list. diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 62e9979d3d..b010eeaa08 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -780,6 +780,17 @@ func TestEstimateGas(t *testing.T) { expectErr: core.ErrInsufficientFunds, want: 21000, }, + // block override gas limit should bound estimation search space. + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + Input: hex2Bytes("6080604052348015600f57600080fd5b50483a1015601c57600080fd5b60003a111560315760004811603057600080fd5b5b603f80603e6000396000f3fe6080604052600080fdfea264697066735822122060729c2cee02b10748fae5200f1c9da4661963354973d9154c13a8e9ce9dee1564736f6c63430008130033"), + Gas: func() *hexutil.Uint64 { v := hexutil.Uint64(0); return &v }(), + }, + blockOverrides: override.BlockOverrides{GasLimit: func() *hexutil.Uint64 { v := hexutil.Uint64(50000); return &v }()}, + expectErr: errors.New("gas required exceeds allowance (50000)"), + }, // empty create { blockNumber: rpc.LatestBlockNumber, @@ -861,6 +872,19 @@ func TestEstimateGas(t *testing.T) { }, want: 21000, }, + // blob base fee block override should be applied during estimation. + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1)), + BlobHashes: []common.Hash{{0x01, 0x22}}, + BlobFeeCap: (*hexutil.Big)(big.NewInt(1)), + }, + blockOverrides: override.BlockOverrides{BlobBaseFee: (*hexutil.Big)(big.NewInt(2))}, + expectErr: core.ErrBlobFeeCapTooLow, + }, // // SPDX-License-Identifier: GPL-3.0 //pragma solidity >=0.8.2 <0.9.0; // @@ -1014,7 +1038,7 @@ func TestCall(t *testing.T) { Balance: big.NewInt(params.Ether), Nonce: 1, Storage: map[common.Hash]common.Hash{ - common.Hash{}: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"), + {}: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"), }, }, }, @@ -3795,7 +3819,7 @@ func TestCreateAccessListWithStateOverrides(t *testing.T) { Balance: (*hexutil.Big)(big.NewInt(1000000000000000000)), Nonce: &nonce, State: map[common.Hash]common.Hash{ - common.Hash{}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000002a"), + {}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000002a"), }, }, } From db6c7d06a2b3d18a17a0a79468fe76a355e78e14 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 2 Apr 2026 06:21:58 +0800 Subject: [PATCH 06/27] triedb/pathdb: implement history index pruner (#33999) This PR implements the missing functionality for archive nodes by pruning stale index data. The current mechanism is relatively simple but sufficient for now: it periodically iterates over index entries and deletes outdated data on a per-block basis. The pruning process is triggered every 90,000 new blocks (approximately every 12 days), and the iteration typically takes ~30 minutes on a mainnet node. This mechanism is only applied with `gcmode=archive` enabled, having no impact on normal full node. --- triedb/pathdb/disklayer.go | 5 + triedb/pathdb/history_index_pruner.go | 385 +++++++++++++++++++++ triedb/pathdb/history_index_pruner_test.go | 355 +++++++++++++++++++ triedb/pathdb/history_indexer.go | 20 ++ triedb/pathdb/metrics.go | 10 +- 5 files changed, 771 insertions(+), 4 deletions(-) create mode 100644 triedb/pathdb/history_index_pruner.go create mode 100644 triedb/pathdb/history_index_pruner_test.go diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 5bad19b4f5..50c7279d0e 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -408,6 +408,11 @@ func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error if err != nil { return false, err } + // Notify the index pruner about the new tail so that stale index + // blocks referencing the pruned histories can be cleaned up. + if indexer != nil && pruned > 0 { + indexer.prune(newFirst) + } log.Debug("Pruned history", "type", typ, "items", pruned, "tailid", newFirst) return false, nil } diff --git a/triedb/pathdb/history_index_pruner.go b/triedb/pathdb/history_index_pruner.go new file mode 100644 index 0000000000..c9be3618e8 --- /dev/null +++ b/triedb/pathdb/history_index_pruner.go @@ -0,0 +1,385 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "encoding/binary" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +const ( + // indexPruningThreshold defines the number of pruned histories that must + // accumulate before triggering index pruning. This helps avoid scheduling + // index pruning too frequently. + indexPruningThreshold = 90000 + + // iteratorReopenInterval is how long the iterator is kept open before + // being released and re-opened. Long-lived iterators hold a read snapshot + // that blocks LSM compaction; periodically re-opening avoids stalling the + // compactor during a large scan. + iteratorReopenInterval = 30 * time.Second +) + +// indexPruner is responsible for pruning stale index data from the tail side +// when old history objects are removed. It runs as a background goroutine and +// processes pruning signals whenever the history tail advances. +// +// The pruning operates at the block level: for each state element's index +// metadata, leading index blocks whose maximum history ID falls below the +// new tail are removed entirely. This avoids the need to decode individual +// block contents and is efficient because index blocks store monotonically +// increasing history IDs. +type indexPruner struct { + disk ethdb.KeyValueStore + typ historyType + tail atomic.Uint64 // Tail below which index entries can be pruned + lastRun uint64 // The tail in the last pruning run + trigger chan struct{} // Non-blocking signal that tail has advanced + closed chan struct{} + wg sync.WaitGroup + log log.Logger + + pauseReq chan chan struct{} // Pause request; caller sends ack channel, pruner closes it when paused + resumeCh chan struct{} // Resume signal sent by caller after indexSingle/unindexSingle completes +} + +// newIndexPruner creates and starts a new index pruner for the given history type. +func newIndexPruner(disk ethdb.KeyValueStore, typ historyType) *indexPruner { + p := &indexPruner{ + disk: disk, + typ: typ, + trigger: make(chan struct{}, 1), + closed: make(chan struct{}), + log: log.New("type", typ.String()), + pauseReq: make(chan chan struct{}), + resumeCh: make(chan struct{}), + } + p.wg.Add(1) + go p.run() + return p +} + +// prune signals the pruner that the history tail has advanced to the given ID. +// All index entries referencing history IDs below newTail can be removed. +func (p *indexPruner) prune(newTail uint64) { + // Only update if the tail is actually advancing + for { + old := p.tail.Load() + if newTail <= old { + return + } + if p.tail.CompareAndSwap(old, newTail) { + break + } + } + // Non-blocking signal + select { + case p.trigger <- struct{}{}: + default: + } +} + +// pause requests the pruner to flush all pending writes and pause. It blocks +// until the pruner has acknowledged the pause. This must be paired with a +// subsequent call to resume. +func (p *indexPruner) pause() { + ack := make(chan struct{}) + select { + case p.pauseReq <- ack: + <-ack // wait for the pruner to flush and acknowledge + case <-p.closed: + } +} + +// resume unblocks a previously paused pruner, allowing it to continue +// processing. +func (p *indexPruner) resume() { + select { + case p.resumeCh <- struct{}{}: + case <-p.closed: + } +} + +// close shuts down the pruner and waits for it to finish. +func (p *indexPruner) close() { + select { + case <-p.closed: + return + default: + close(p.closed) + p.wg.Wait() + } +} + +// run is the main loop of the pruner. It waits for trigger signals and +// processes a small batch of entries on each trigger, advancing the cursor. +func (p *indexPruner) run() { + defer p.wg.Done() + + for { + select { + case <-p.trigger: + tail := p.tail.Load() + if tail < p.lastRun || tail-p.lastRun < indexPruningThreshold { + continue + } + if err := p.process(tail); err != nil { + p.log.Error("Failed to prune index", "tail", tail, "err", err) + } else { + p.lastRun = tail + } + + case ack := <-p.pauseReq: + // Pruner is idle, acknowledge immediately and wait for resume. + close(ack) + select { + case <-p.resumeCh: + case <-p.closed: + return + } + + case <-p.closed: + return + } + } +} + +// process iterates all index metadata entries for the history type and prunes +// leading blocks whose max history ID is below the given tail. +func (p *indexPruner) process(tail uint64) error { + var ( + err error + pruned int + start = time.Now() + ) + switch p.typ { + case typeStateHistory: + n, err := p.prunePrefix(rawdb.StateHistoryAccountMetadataPrefix, typeAccount, tail) + if err != nil { + return err + } + pruned += n + + n, err = p.prunePrefix(rawdb.StateHistoryStorageMetadataPrefix, typeStorage, tail) + if err != nil { + return err + } + pruned += n + statePruneHistoryIndexTimer.UpdateSince(start) + + case typeTrienodeHistory: + pruned, err = p.prunePrefix(rawdb.TrienodeHistoryMetadataPrefix, typeTrienode, tail) + if err != nil { + return err + } + trienodePruneHistoryIndexTimer.UpdateSince(start) + + default: + panic("unknown history type") + } + if pruned > 0 { + p.log.Info("Pruned stale index blocks", "pruned", pruned, "tail", tail, "elapsed", common.PrettyDuration(time.Since(start))) + } + return nil +} + +// prunePrefix scans all metadata entries under the given prefix and prunes +// leading index blocks below the tail. The iterator is periodically released +// and re-opened to avoid holding a read snapshot that blocks LSM compaction. +func (p *indexPruner) prunePrefix(prefix []byte, elemType elementType, tail uint64) (int, error) { + var ( + pruned int + opened = time.Now() + it = p.disk.NewIterator(prefix, nil) + batch = p.disk.NewBatchWithSize(ethdb.IdealBatchSize) + ) + for { + // Terminate if iterator is exhausted + if !it.Next() { + it.Release() + break + } + // Check termination or pause request + select { + case <-p.closed: + // Terminate the process if indexer is closed + it.Release() + if batch.ValueSize() > 0 { + return pruned, batch.Write() + } + return pruned, nil + + case ack := <-p.pauseReq: + // Save the current position so that after resume the + // iterator can be re-opened from where it left off. + start := common.CopyBytes(it.Key()[len(prefix):]) + it.Release() + + // Flush all pending writes before acknowledging the pause. + var flushErr error + if batch.ValueSize() > 0 { + if err := batch.Write(); err != nil { + flushErr = err + } + batch.Reset() + } + close(ack) + + // Block until resumed or closed. Always wait here even if + // the flush failed — returning early would cause resume() + // to deadlock since nobody would receive on resumeCh. + select { + case <-p.resumeCh: + if flushErr != nil { + return 0, flushErr + } + // Re-open the iterator from the saved position so the + // pruner sees the current database state (including any + // writes made by indexer during the pause). + it = p.disk.NewIterator(prefix, start) + opened = time.Now() + continue + case <-p.closed: + return pruned, flushErr + } + + default: + // Keep processing + } + + // Prune the index data block + key, value := it.Key(), it.Value() + ident, bsize := p.identFromKey(key, prefix, elemType) + n, err := p.pruneEntry(batch, ident, value, bsize, tail) + if err != nil { + p.log.Warn("Failed to prune index entry", "ident", ident, "err", err) + continue + } + pruned += n + + // Flush the batch if there are too many accumulated + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + it.Release() + return 0, err + } + batch.Reset() + } + + // Periodically release the iterator so the LSM compactor + // is not blocked by the read snapshot we hold. + if time.Since(opened) >= iteratorReopenInterval { + opened = time.Now() + + start := common.CopyBytes(it.Key()[len(prefix):]) + it.Release() + it = p.disk.NewIterator(prefix, start) + } + } + if batch.ValueSize() > 0 { + if err := batch.Write(); err != nil { + return 0, err + } + } + return pruned, nil +} + +// identFromKey reconstructs the stateIdent and bitmapSize from a metadata key. +func (p *indexPruner) identFromKey(key []byte, prefix []byte, elemType elementType) (stateIdent, int) { + rest := key[len(prefix):] + + switch elemType { + case typeAccount: + // key = prefix + addressHash(32) + var addrHash common.Hash + copy(addrHash[:], rest[:32]) + return newAccountIdent(addrHash), 0 + + case typeStorage: + // key = prefix + addressHash(32) + storageHash(32) + var addrHash, storHash common.Hash + copy(addrHash[:], rest[:32]) + copy(storHash[:], rest[32:64]) + return newStorageIdent(addrHash, storHash), 0 + + case typeTrienode: + // key = prefix + addressHash(32) + path(variable) + var addrHash common.Hash + copy(addrHash[:], rest[:32]) + path := string(rest[32:]) + ident := newTrienodeIdent(addrHash, path) + return ident, ident.bloomSize() + + default: + panic("unknown element type") + } +} + +// pruneEntry checks a single metadata entry and removes leading index blocks +// whose max < tail. Returns the number of blocks pruned. +func (p *indexPruner) pruneEntry(batch ethdb.Batch, ident stateIdent, blob []byte, bsize int, tail uint64) (int, error) { + // Fast path: the first 8 bytes of the metadata encode the max history ID + // of the first index block (big-endian uint64). If it is >= tail, no + // blocks can be pruned and we skip the full parse entirely. + if len(blob) >= 8 && binary.BigEndian.Uint64(blob[:8]) >= tail { + return 0, nil + } + descList, err := parseIndex(blob, bsize) + if err != nil { + return 0, err + } + // Find the number of leading blocks that can be entirely pruned. + // A block can be pruned if its max history ID is strictly below + // the tail. + var count int + for _, desc := range descList { + if desc.max < tail { + count++ + } else { + break // blocks are ordered, no more to prune + } + } + if count == 0 { + return 0, nil + } + // Delete the pruned index blocks + for i := 0; i < count; i++ { + deleteStateIndexBlock(ident, batch, descList[i].id) + } + // Update or delete the metadata + remaining := descList[count:] + if len(remaining) == 0 { + // All blocks pruned, remove the metadata entry entirely + deleteStateIndex(ident, batch) + } else { + // Rewrite the metadata with the remaining blocks + size := indexBlockDescSize + bsize + buf := make([]byte, 0, size*len(remaining)) + for _, desc := range remaining { + buf = append(buf, desc.encode()...) + } + writeStateIndex(ident, batch, buf) + } + return count, nil +} diff --git a/triedb/pathdb/history_index_pruner_test.go b/triedb/pathdb/history_index_pruner_test.go new file mode 100644 index 0000000000..b3094de3e6 --- /dev/null +++ b/triedb/pathdb/history_index_pruner_test.go @@ -0,0 +1,355 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "math" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +func writeMultiBlockIndex(t *testing.T, db ethdb.Database, ident stateIdent, bitmapSize int, startID uint64) []*indexBlockDesc { + t.Helper() + + if startID == 0 { + startID = 1 + } + iw, _ := newIndexWriter(db, ident, 0, bitmapSize) + + for i := 0; i < 10000; i++ { + if err := iw.append(startID+uint64(i), randomExt(bitmapSize, 5)); err != nil { + t.Fatalf("Failed to append element %d: %v", i, err) + } + } + batch := db.NewBatch() + iw.finish(batch) + if err := batch.Write(); err != nil { + t.Fatalf("Failed to write batch: %v", err) + } + + blob := readStateIndex(ident, db) + descList, err := parseIndex(blob, bitmapSize) + if err != nil { + t.Fatalf("Failed to parse index: %v", err) + } + return descList +} + +// TestPruneEntryBasic verifies that pruneEntry correctly removes leading index +// blocks whose max is below the given tail. +func TestPruneEntryBasic(t *testing.T) { + db := rawdb.NewMemoryDatabase() + ident := newAccountIdent(common.Hash{0xa}) + descList := writeMultiBlockIndex(t, db, ident, 0, 1) + + // Prune with a tail that is above the first block's max but below the second + firstBlockMax := descList[0].max + + pruner := newIndexPruner(db, typeStateHistory) + defer pruner.close() + + if err := pruner.process(firstBlockMax + 1); err != nil { + t.Fatalf("Failed to process pruning: %v", err) + } + + // Verify the first block was removed + blob := readStateIndex(ident, db) + if len(blob) == 0 { + t.Fatal("Index metadata should not be empty after partial prune") + } + remaining, err := parseIndex(blob, 0) + if err != nil { + t.Fatalf("Failed to parse index after prune: %v", err) + } + if len(remaining) != len(descList)-1 { + t.Fatalf("Expected %d blocks remaining, got %d", len(descList)-1, len(remaining)) + } + // The first remaining block should be what was previously the second block + if remaining[0].id != descList[1].id { + t.Fatalf("Expected first remaining block id %d, got %d", descList[1].id, remaining[0].id) + } + + // Verify the pruned block data is actually deleted + blockData := readStateIndexBlock(ident, db, descList[0].id) + if len(blockData) != 0 { + t.Fatal("Pruned block data should have been deleted") + } + + // Remaining blocks should still have their data + for _, desc := range remaining { + blockData = readStateIndexBlock(ident, db, desc.id) + if len(blockData) == 0 { + t.Fatalf("Block %d data should still exist", desc.id) + } + } +} + +// TestPruneEntryBasicTrienode is the same as TestPruneEntryBasic but for +// trienode index entries with a non-zero bitmapSize. +func TestPruneEntryBasicTrienode(t *testing.T) { + db := rawdb.NewMemoryDatabase() + addrHash := common.Hash{0xa} + path := string([]byte{0x0, 0x0, 0x0}) + ident := newTrienodeIdent(addrHash, path) + + descList := writeMultiBlockIndex(t, db, ident, ident.bloomSize(), 1) + firstBlockMax := descList[0].max + + pruner := newIndexPruner(db, typeTrienodeHistory) + defer pruner.close() + + if err := pruner.process(firstBlockMax + 1); err != nil { + t.Fatalf("Failed to process pruning: %v", err) + } + + blob := readStateIndex(ident, db) + remaining, err := parseIndex(blob, ident.bloomSize()) + if err != nil { + t.Fatalf("Failed to parse index after prune: %v", err) + } + if len(remaining) != len(descList)-1 { + t.Fatalf("Expected %d blocks remaining, got %d", len(descList)-1, len(remaining)) + } + if remaining[0].id != descList[1].id { + t.Fatalf("Expected first remaining block id %d, got %d", descList[1].id, remaining[0].id) + } + blockData := readStateIndexBlock(ident, db, descList[0].id) + if len(blockData) != 0 { + t.Fatal("Pruned block data should have been deleted") + } +} + +// TestPruneEntryComplete verifies that when all blocks are pruned, the metadata +// entry is also deleted. +func TestPruneEntryComplete(t *testing.T) { + db := rawdb.NewMemoryDatabase() + ident := newAccountIdent(common.Hash{0xb}) + iw, _ := newIndexWriter(db, ident, 0, 0) + + for i := 1; i <= 10; i++ { + if err := iw.append(uint64(i), nil); err != nil { + t.Fatalf("Failed to append: %v", err) + } + } + batch := db.NewBatch() + iw.finish(batch) + if err := batch.Write(); err != nil { + t.Fatalf("Failed to write: %v", err) + } + + pruner := newIndexPruner(db, typeStateHistory) + defer pruner.close() + + // Prune with tail above all elements + if err := pruner.process(11); err != nil { + t.Fatalf("Failed to process: %v", err) + } + + // Metadata entry should be deleted + blob := readStateIndex(ident, db) + if len(blob) != 0 { + t.Fatal("Index metadata should be empty after full prune") + } +} + +// TestPruneNoop verifies that pruning does nothing when the tail is below all +// block maximums. +func TestPruneNoop(t *testing.T) { + db := rawdb.NewMemoryDatabase() + ident := newAccountIdent(common.Hash{0xc}) + iw, _ := newIndexWriter(db, ident, 0, 0) + + for i := 100; i <= 200; i++ { + if err := iw.append(uint64(i), nil); err != nil { + t.Fatalf("Failed to append: %v", err) + } + } + batch := db.NewBatch() + iw.finish(batch) + if err := batch.Write(); err != nil { + t.Fatalf("Failed to write: %v", err) + } + + blob := readStateIndex(ident, db) + origLen := len(blob) + + pruner := newIndexPruner(db, typeStateHistory) + defer pruner.close() + + if err := pruner.process(50); err != nil { + t.Fatalf("Failed to process: %v", err) + } + + // Nothing should have changed + blob = readStateIndex(ident, db) + if len(blob) != origLen { + t.Fatalf("Expected no change, original len %d, got %d", origLen, len(blob)) + } +} + +// TestPrunePreservesReadability verifies that after pruning, the remaining +// index data is still readable and returns correct results. +func TestPrunePreservesReadability(t *testing.T) { + db := rawdb.NewMemoryDatabase() + ident := newAccountIdent(common.Hash{0xe}) + descList := writeMultiBlockIndex(t, db, ident, 0, 1) + firstBlockMax := descList[0].max + + pruner := newIndexPruner(db, typeStateHistory) + defer pruner.close() + + if err := pruner.process(firstBlockMax + 1); err != nil { + t.Fatalf("Failed to process: %v", err) + } + + // Read the remaining index and verify lookups still work + ir, err := newIndexReader(db, ident, 0) + if err != nil { + t.Fatalf("Failed to create reader: %v", err) + } + + // Looking for something greater than firstBlockMax should still work + result, err := ir.readGreaterThan(firstBlockMax) + if err != nil { + t.Fatalf("Failed to read: %v", err) + } + if result != firstBlockMax+1 { + t.Fatalf("Expected %d, got %d", firstBlockMax+1, result) + } + + // Looking for the last element should return MaxUint64 + result, err = ir.readGreaterThan(20000) + if err != nil { + t.Fatalf("Failed to read: %v", err) + } + if result != math.MaxUint64 { + t.Fatalf("Expected MaxUint64, got %d", result) + } +} + +// TestPrunePauseResume verifies the pause/resume mechanism: +// - The pruner pauses mid-iteration and flushes its batch +// - Data written while the pruner is paused (simulating indexSingle) is +// visible after resume via a fresh iterator +// - Pruning still completes correctly after resume +func TestPrunePauseResume(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + // Create many accounts with multi-block indexes so the pruner is still + // iterating when the pause request arrives. + var firstBlockMax uint64 + for i := 0; i < 200; i++ { + hash := common.Hash{byte(i)} + ident := newAccountIdent(hash) + descList := writeMultiBlockIndex(t, db, ident, 0, 1) + if i == 0 { + firstBlockMax = descList[0].max + } + } + // Target account at the end of the key space — the pruner should not + // have visited it yet when the pause is acknowledged. + targetIdent := newAccountIdent(common.Hash{0xff}) + targetDescList := writeMultiBlockIndex(t, db, targetIdent, 0, 1) + + tail := firstBlockMax + 1 + + // Construct the pruner without starting run(). We call process() + // directly to exercise the mid-iteration pause path deterministically. + pruner := &indexPruner{ + disk: db, + typ: typeStateHistory, + log: log.New("type", "account"), + closed: make(chan struct{}), + pauseReq: make(chan chan struct{}, 1), // buffered so we can pre-deposit + resumeCh: make(chan struct{}), + } + + // Pre-deposit a pause request before process() starts. Because + // pauseReq is buffered, this succeeds immediately. When prunePrefix's + // select checks the channel on an early iteration, it will find the + // pending request and pause — no scheduling race is possible. + ack := make(chan struct{}) + pruner.pauseReq <- ack + + // Run process() in the background. + errCh := make(chan error, 1) + go func() { + errCh <- pruner.process(tail) + }() + + // Block until the pruner has flushed pending writes and acknowledged. + <-ack + + // While paused, append a new element to the target account's index, + // simulating what indexSingle would do during the pause window. + lastMax := targetDescList[len(targetDescList)-1].max + newID := lastMax + 10000 + iw, err := newIndexWriter(db, targetIdent, lastMax, 0) + if err != nil { + t.Fatalf("Failed to create index writer: %v", err) + } + if err := iw.append(newID, nil); err != nil { + t.Fatalf("Failed to append: %v", err) + } + batch := db.NewBatch() + iw.finish(batch) + if err := batch.Write(); err != nil { + t.Fatalf("Failed to write batch: %v", err) + } + + // Resume the pruner. + pruner.resume() + + // Wait for process() to complete. + if err := <-errCh; err != nil { + t.Fatalf("process() failed: %v", err) + } + + // Verify: the entry written during the pause must still be accessible. + // If the pruner used a stale iterator snapshot, it would overwrite the + // target's metadata and lose the new entry. + ir, err := newIndexReader(db, targetIdent, 0) + if err != nil { + t.Fatalf("Failed to create index reader: %v", err) + } + result, err := ir.readGreaterThan(newID - 1) + if err != nil { + t.Fatalf("Failed to read: %v", err) + } + if result != newID { + t.Fatalf("Entry written during pause was lost: want %d, got %d", newID, result) + } + + // Verify: pruning actually occurred on an early account. + earlyIdent := newAccountIdent(common.Hash{0x00}) + earlyBlob := readStateIndex(earlyIdent, db) + if len(earlyBlob) == 0 { + t.Fatal("Early account index should not be completely empty") + } + earlyRemaining, err := parseIndex(earlyBlob, 0) + if err != nil { + t.Fatalf("Failed to parse early account index: %v", err) + } + // The first block (id=0) should have been pruned. + if earlyRemaining[0].id == 0 { + t.Fatal("First block of early account should have been pruned") + } +} diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index c9bf3e87f1..9b215b917f 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -719,6 +719,7 @@ func (i *indexIniter) recover() bool { // state history. type historyIndexer struct { initer *indexIniter + pruner *indexPruner typ historyType disk ethdb.KeyValueStore freezer ethdb.AncientStore @@ -774,6 +775,7 @@ func newHistoryIndexer(disk ethdb.Database, freezer ethdb.AncientStore, lastHist checkVersion(disk, typ) return &historyIndexer{ initer: newIndexIniter(disk, freezer, typ, lastHistoryID, noWait), + pruner: newIndexPruner(disk, typ), typ: typ, disk: disk, freezer: freezer, @@ -782,6 +784,7 @@ func newHistoryIndexer(disk ethdb.Database, freezer ethdb.AncientStore, lastHist func (i *historyIndexer) close() { i.initer.close() + i.pruner.close() } // inited returns a flag indicating whether the existing state histories @@ -802,6 +805,8 @@ func (i *historyIndexer) extend(historyID uint64) error { case <-i.initer.closed: return errors.New("indexer is closed") case <-i.initer.done: + i.pruner.pause() + defer i.pruner.resume() return indexSingle(historyID, i.disk, i.freezer, i.typ) case i.initer.interrupt <- signal: return <-signal.result @@ -819,12 +824,27 @@ func (i *historyIndexer) shorten(historyID uint64) error { case <-i.initer.closed: return errors.New("indexer is closed") case <-i.initer.done: + i.pruner.pause() + defer i.pruner.resume() return unindexSingle(historyID, i.disk, i.freezer, i.typ) case i.initer.interrupt <- signal: return <-signal.result } } +// prune signals the pruner that the history tail has advanced to the given ID, +// so that stale index blocks referencing pruned histories can be removed. +func (i *historyIndexer) prune(newTail uint64) { + select { + case <-i.initer.closed: + log.Debug("Ignored the pruning signal", "reason", "closed") + case <-i.initer.done: + i.pruner.prune(newTail) + default: + log.Debug("Ignored the pruning signal", "reason", "busy") + } +} + // progress returns the indexing progress made so far. It provides the number // of states that remain unindexed. func (i *historyIndexer) progress() (uint64, error) { diff --git a/triedb/pathdb/metrics.go b/triedb/pathdb/metrics.go index a0a626f9b5..e01dfdfb86 100644 --- a/triedb/pathdb/metrics.go +++ b/triedb/pathdb/metrics.go @@ -77,10 +77,12 @@ var ( trienodeHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/data", nil) trienodeHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/index", nil) - stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil) - stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil) - trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil) - trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil) + stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil) + stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil) + statePruneHistoryIndexTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/prune/time", nil) + trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil) + trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil) + trienodePruneHistoryIndexTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/prune/time", nil) lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil) lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil) From bcb0efd756b00112422a50919d2a00810ad662ad Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Apr 2026 20:40:45 +0800 Subject: [PATCH 07/27] core/types: copy block access list hash in CopyHeader (#34636) --- core/types/block.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/types/block.go b/core/types/block.go index 2d65adeff3..60c83c9db1 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -325,6 +325,10 @@ func CopyHeader(h *Header) *Header { cpy.RequestsHash = new(common.Hash) *cpy.RequestsHash = *h.RequestsHash } + if h.BlockAccessListHash != nil { + cpy.BlockAccessListHash = new(common.Hash) + *cpy.BlockAccessListHash = *h.BlockAccessListHash + } if h.SlotNumber != nil { cpy.SlotNumber = new(uint64) *cpy.SlotNumber = *h.SlotNumber From 0ba4314321c3f36b2cc82e71afcb3b77115c257b Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 3 Apr 2026 10:35:32 +0800 Subject: [PATCH 08/27] core/state: introduce state iterator interface (#33102) In this PR, the Database interface in `core/state` has been extended with one more function: ```go // Iteratee returns a state iteratee associated with the specified state root, // through which the account iterator and storage iterator can be created. Iteratee(root common.Hash) (Iteratee, error) ``` With this additional abstraction layer, the implementation details can be hidden behind the interface. For example, state traversal can now operate directly on the flat state for Verkle or binary trees, which do not natively support traversal. Moreover, state dumping will now prefer using the flat state iterator as the primary option, offering better efficiency. Edit: this PR also fixes a tiny issue in the state dump, marshalling the next field in the correct way. --- core/state/database.go | 13 +- core/state/database_history.go | 12 +- core/state/database_iterator.go | 435 +++++++++++++++++++++++++++ core/state/database_iterator_test.go | 262 ++++++++++++++++ core/state/dump.go | 65 ++-- core/state/statedb.go | 98 ++---- core/state/statedb_test.go | 4 +- eth/api_debug.go | 2 + 8 files changed, 763 insertions(+), 128 deletions(-) create mode 100644 core/state/database_iterator.go create mode 100644 core/state/database_iterator_test.go diff --git a/core/state/database.go b/core/state/database.go index 002ce57fbc..c603e3ad7a 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -39,6 +39,10 @@ type Database interface { // Reader returns a state reader associated with the specified state root. Reader(root common.Hash) (Reader, error) + // Iteratee returns a state iteratee associated with the specified state root, + // through which the account iterator and storage iterator can be created. + Iteratee(root common.Hash) (Iteratee, error) + // OpenTrie opens the main account trie. OpenTrie(root common.Hash) (Trie, error) @@ -48,9 +52,6 @@ type Database interface { // TrieDB returns the underlying trie database for managing trie nodes. TrieDB() *triedb.Database - // Snapshot returns the underlying state snapshot. - Snapshot() *snapshot.Tree - // Commit flushes all pending writes and finalizes the state transition, // committing the changes to the underlying storage. It returns an error // if the commit fails. @@ -310,6 +311,12 @@ func (db *CachingDB) Commit(update *stateUpdate) error { return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet()) } +// Iteratee returns a state iteratee associated with the specified state root, +// through which the account iterator and storage iterator can be created. +func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) { + return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap) +} + // mustCopyTrie returns a deep-copied trie. func mustCopyTrie(t Trie) Trie { switch t := t.(type) { diff --git a/core/state/database_history.go b/core/state/database_history.go index c25c4eae4b..0dbb8cc546 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -22,7 +22,6 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" @@ -289,14 +288,15 @@ func (db *HistoricDB) TrieDB() *triedb.Database { return db.triedb } -// Snapshot returns the underlying state snapshot. -func (db *HistoricDB) Snapshot() *snapshot.Tree { - return nil -} - // Commit flushes all pending writes and finalizes the state transition, // committing the changes to the underlying storage. It returns an error // if the commit fails. func (db *HistoricDB) Commit(update *stateUpdate) error { return errors.New("not implemented") } + +// Iteratee returns a state iteratee associated with the specified state root, +// through which the account iterator and storage iterator can be created. +func (db *HistoricDB) Iteratee(root common.Hash) (Iteratee, error) { + return nil, errors.New("not implemented") +} diff --git a/core/state/database_iterator.go b/core/state/database_iterator.go new file mode 100644 index 0000000000..8fad66a1e8 --- /dev/null +++ b/core/state/database_iterator.go @@ -0,0 +1,435 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/triedb" +) + +// Iterator is an iterator to step over all the accounts or the specific +// storage in the specific state. +type Iterator interface { + // Next steps the iterator forward one element. It returns false if the iterator + // is exhausted or if an error occurs. Any error encountered is retained and + // can be retrieved via Error(). + Next() bool + + // Error returns any failure that occurred during iteration, which might have + // caused a premature iteration exit. + Error() error + + // Hash returns the hash of the account or storage slot the iterator is + // currently at. + Hash() common.Hash + + // Release releases associated resources. Release should always succeed and + // can be called multiple times without causing error. + Release() +} + +// AccountIterator is an iterator to step over all the accounts in the +// specific state. +type AccountIterator interface { + Iterator + + // Address returns the raw account address the iterator is currently at. + // An error will be returned if the preimage is not available. + Address() (common.Address, error) + + // Account returns the RLP encoded account the iterator is currently at. + // An error will be retained if the iterator becomes invalid. + Account() []byte +} + +// StorageIterator is an iterator to step over the specific storage in the +// specific state. +type StorageIterator interface { + Iterator + + // Key returns the raw storage slot key the iterator is currently at. + // An error will be returned if the preimage is not available. + Key() (common.Hash, error) + + // Slot returns the storage slot the iterator is currently at. An error will + // be retained if the iterator becomes invalid. + Slot() []byte +} + +// Iteratee wraps the NewIterator methods for traversing the accounts and +// storages of the specific state. +type Iteratee interface { + // NewAccountIterator creates an account iterator for the state specified by + // the given root. It begins at a specified starting position, corresponding + // to a particular initial key (or the next key if the specified one does + // not exist). + // + // The starting position here refers to the hash of the account address. + NewAccountIterator(start common.Hash) (AccountIterator, error) + + // NewStorageIterator creates a storage iterator for the state specified by + // the address hash. It begins at a specified starting position, corresponding + // to a particular initial key (or the next key if the specified one does + // not exist). + // + // The starting position here refers to the hash of the slot key. + NewStorageIterator(addressHash common.Hash, start common.Hash) (StorageIterator, error) +} + +// PreimageReader wraps the function Preimage for accessing the preimage of +// a given hash. +type PreimageReader interface { + // Preimage returns the preimage of associated hash. + Preimage(hash common.Hash) []byte +} + +// flatAccountIterator is a wrapper around the underlying flat state iterator. +// Before returning data from the iterator, it performs an additional conversion +// to bridge the slim encoding with the full encoding format. +type flatAccountIterator struct { + err error + it snapshot.AccountIterator + preimage PreimageReader +} + +// newFlatAccountIterator constructs the account iterator with the provided +// flat state iterator. +func newFlatAccountIterator(it snapshot.AccountIterator, preimage PreimageReader) *flatAccountIterator { + return &flatAccountIterator{it: it, preimage: preimage} +} + +// Next steps the iterator forward one element. It returns false if the iterator +// is exhausted or if an error occurs. Any error encountered is retained and +// can be retrieved via Error(). +func (ai *flatAccountIterator) Next() bool { + if ai.err != nil { + return false + } + return ai.it.Next() +} + +// Error returns any failure that occurred during iteration, which might have +// caused a premature iteration exit. +func (ai *flatAccountIterator) Error() error { + if ai.err != nil { + return ai.err + } + return ai.it.Error() +} + +// Hash returns the hash of the account or storage slot the iterator is +// currently at. +func (ai *flatAccountIterator) Hash() common.Hash { + return ai.it.Hash() +} + +// Release releases associated resources. Release should always succeed and +// can be called multiple times without causing error. +func (ai *flatAccountIterator) Release() { + ai.it.Release() +} + +// Address returns the raw account address the iterator is currently at. +// An error will be returned if the preimage is not available. +func (ai *flatAccountIterator) Address() (common.Address, error) { + if ai.preimage == nil { + return common.Address{}, errors.New("account address is not available") + } + preimage := ai.preimage.Preimage(ai.Hash()) + if preimage == nil { + return common.Address{}, errors.New("account address is not available") + } + return common.BytesToAddress(preimage), nil +} + +// Account returns the account data the iterator is currently at. The account +// data is encoded as slim format from the underlying iterator, the conversion +// is required. +func (ai *flatAccountIterator) Account() []byte { + data, err := types.FullAccountRLP(ai.it.Account()) + if err != nil { + ai.err = err + return nil + } + return data +} + +// flatStorageIterator is a wrapper around the underlying flat state iterator. +type flatStorageIterator struct { + it snapshot.StorageIterator + preimage PreimageReader +} + +// newFlatStorageIterator constructs the storage iterator with the provided +// flat state iterator. +func newFlatStorageIterator(it snapshot.StorageIterator, preimage PreimageReader) *flatStorageIterator { + return &flatStorageIterator{it: it, preimage: preimage} +} + +// Next steps the iterator forward one element. It returns false if the iterator +// is exhausted or if an error occurs. Any error encountered is retained and +// can be retrieved via Error(). +func (si *flatStorageIterator) Next() bool { + return si.it.Next() +} + +// Error returns any failure that occurred during iteration, which might have +// caused a premature iteration exit. +func (si *flatStorageIterator) Error() error { + return si.it.Error() +} + +// Hash returns the hash of the account or storage slot the iterator is +// currently at. +func (si *flatStorageIterator) Hash() common.Hash { + return si.it.Hash() +} + +// Release releases associated resources. Release should always succeed and +// can be called multiple times without causing error. +func (si *flatStorageIterator) Release() { + si.it.Release() +} + +// Key returns the raw storage slot key the iterator is currently at. +// An error will be returned if the preimage is not available. +func (si *flatStorageIterator) Key() (common.Hash, error) { + if si.preimage == nil { + return common.Hash{}, errors.New("slot key is not available") + } + preimage := si.preimage.Preimage(si.Hash()) + if preimage == nil { + return common.Hash{}, errors.New("slot key is not available") + } + return common.BytesToHash(preimage), nil +} + +// Slot returns the storage slot data the iterator is currently at. +func (si *flatStorageIterator) Slot() []byte { + return si.it.Slot() +} + +// merkleIterator implements the Iterator interface, providing functions to traverse +// the accounts or storages with the manner of Merkle-Patricia-Trie. +type merkleIterator struct { + tr Trie + it *trie.Iterator + account bool +} + +// newMerkleTrieIterator constructs the iterator with the given trie and starting position. +func newMerkleTrieIterator(tr Trie, start common.Hash, account bool) (*merkleIterator, error) { + it, err := tr.NodeIterator(start.Bytes()) + if err != nil { + return nil, err + } + return &merkleIterator{ + tr: tr, + it: trie.NewIterator(it), + account: account, + }, nil +} + +// Next steps the iterator forward one element. It returns false if the iterator +// is exhausted or if an error occurs. Any error encountered is retained and +// can be retrieved via Error(). +func (ti *merkleIterator) Next() bool { + return ti.it.Next() +} + +// Error returns any failure that occurred during iteration, which might have +// caused a premature iteration exit. +func (ti *merkleIterator) Error() error { + return ti.it.Err +} + +// Hash returns the hash of the account or storage slot the iterator is +// currently at. +func (ti *merkleIterator) Hash() common.Hash { + return common.BytesToHash(ti.it.Key) +} + +// Release releases associated resources. Release should always succeed and +// can be called multiple times without causing error. +func (ti *merkleIterator) Release() {} + +// Address returns the raw account address the iterator is currently at. +// An error will be returned if the preimage is not available. +func (ti *merkleIterator) Address() (common.Address, error) { + if !ti.account { + return common.Address{}, errors.New("account address is not available") + } + preimage := ti.tr.GetKey(ti.it.Key) + if preimage == nil { + return common.Address{}, errors.New("account address is not available") + } + return common.BytesToAddress(preimage), nil +} + +// Account returns the account data the iterator is currently at. +func (ti *merkleIterator) Account() []byte { + if !ti.account { + return nil + } + return ti.it.Value +} + +// Key returns the raw storage slot key the iterator is currently at. +// An error will be returned if the preimage is not available. +func (ti *merkleIterator) Key() (common.Hash, error) { + if ti.account { + return common.Hash{}, errors.New("slot key is not available") + } + preimage := ti.tr.GetKey(ti.it.Key) + if preimage == nil { + return common.Hash{}, errors.New("slot key is not available") + } + return common.BytesToHash(preimage), nil +} + +// Slot returns the storage slot the iterator is currently at. +func (ti *merkleIterator) Slot() []byte { + if ti.account { + return nil + } + return ti.it.Value +} + +// stateIteratee implements Iteratee interface, providing the state traversal +// functionalities of a specific state. +type stateIteratee struct { + merkle bool + root common.Hash + triedb *triedb.Database + snap *snapshot.Tree +} + +func newStateIteratee(merkle bool, root common.Hash, triedb *triedb.Database, snap *snapshot.Tree) (*stateIteratee, error) { + return &stateIteratee{ + merkle: merkle, + root: root, + triedb: triedb, + snap: snap, + }, nil +} + +// NewAccountIterator creates an account iterator for the state specified by +// the given root. It begins at a specified starting position, corresponding +// to a particular initial key (or the next key if the specified one does +// not exist). +// +// The starting position here refers to the hash of the account address. +func (si *stateIteratee) NewAccountIterator(start common.Hash) (AccountIterator, error) { + // If the external snapshot is available (hash scheme), try to initialize + // the account iterator from there first. + if si.snap != nil { + it, err := si.snap.AccountIterator(si.root, start) + if err == nil { + return newFlatAccountIterator(it, si.triedb), nil + } + } + // If the external snapshot is not available, try to initialize the + // account iterator from the trie database (path scheme) + it, err := si.triedb.AccountIterator(si.root, start) + if err == nil { + return newFlatAccountIterator(it, si.triedb), nil + } + if !si.merkle { + return nil, fmt.Errorf("state %x is not available for account traversal", si.root) + } + // The snapshot is not usable so far, construct the account iterator from + // the trie as the fallback. It's not as efficient as the flat state iterator. + tr, err := trie.NewStateTrie(trie.StateTrieID(si.root), si.triedb) + if err != nil { + return nil, err + } + return newMerkleTrieIterator(tr, start, true) +} + +// NewStorageIterator creates a storage iterator for the state specified by +// the address hash. It begins at a specified starting position, corresponding +// to a particular initial key (or the next key if the specified one does not exist). +// +// The starting position here refers to the hash of the slot key. +func (si *stateIteratee) NewStorageIterator(addressHash common.Hash, start common.Hash) (StorageIterator, error) { + // If the external snapshot is available (hash scheme), try to initialize + // the storage iterator from there first. + if si.snap != nil { + it, err := si.snap.StorageIterator(si.root, addressHash, start) + if err == nil { + return newFlatStorageIterator(it, si.triedb), nil + } + } + // If the external snapshot is not available, try to initialize the + // storage iterator from the trie database (path scheme) + it, err := si.triedb.StorageIterator(si.root, addressHash, start) + if err == nil { + return newFlatStorageIterator(it, si.triedb), nil + } + if !si.merkle { + return nil, fmt.Errorf("state %x is not available for storage traversal", si.root) + } + // The snapshot is not usable so far, construct the storage iterator from + // the trie as the fallback. It's not as efficient as the flat state iterator. + tr, err := trie.NewStateTrie(trie.StateTrieID(si.root), si.triedb) + if err != nil { + return nil, err + } + acct, err := tr.GetAccountByHash(addressHash) + if err != nil { + return nil, err + } + if acct == nil || acct.Root == types.EmptyRootHash { + return &exhaustedIterator{}, nil + } + storageTr, err := trie.NewStateTrie(trie.StorageTrieID(si.root, addressHash, acct.Root), si.triedb) + if err != nil { + return nil, err + } + return newMerkleTrieIterator(storageTr, start, false) +} + +type exhaustedIterator struct{} + +func (e exhaustedIterator) Next() bool { + return false +} + +func (e exhaustedIterator) Error() error { + return nil +} + +func (e exhaustedIterator) Hash() common.Hash { + return common.Hash{} +} + +func (e exhaustedIterator) Release() { +} + +func (e exhaustedIterator) Key() (common.Hash, error) { + return common.Hash{}, nil +} + +func (e exhaustedIterator) Slot() []byte { + return nil +} diff --git a/core/state/database_iterator_test.go b/core/state/database_iterator_test.go new file mode 100644 index 0000000000..87819e5526 --- /dev/null +++ b/core/state/database_iterator_test.go @@ -0,0 +1,262 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +// TestExhaustedIterator verifies the exhaustedIterator sentinel: Next is false, +// Error is nil, Hash/Key are zero, Slot is nil, and double Release is safe. +func TestExhaustedIterator(t *testing.T) { + var it exhaustedIterator + + if it.Next() { + t.Fatal("Next() returned true") + } + if err := it.Error(); err != nil { + t.Fatalf("Error() = %v, want nil", err) + } + if hash := it.Hash(); hash != (common.Hash{}) { + t.Fatalf("Hash() = %x, want zero", hash) + } + if key, err := it.Key(); key != (common.Hash{}) || err != nil { + t.Fatalf("Key() = %x, %v; want zero, nil", key, err) + } + if slot := it.Slot(); slot != nil { + t.Fatalf("Slot() = %x, want nil", slot) + } + it.Release() + it.Release() +} + +// TestAccountIterator tests the account iterator: correct count, ascending +// hash order, valid full-format RLP, data integrity, address preimage +// resolution, and seek behavior. +func TestAccountIterator(t *testing.T) { + testAccountIterator(t, rawdb.HashScheme) + testAccountIterator(t, rawdb.PathScheme) +} + +func testAccountIterator(t *testing.T, scheme string) { + _, sdb, ndb, root, accounts := makeTestState(scheme) + ndb.Commit(root, false) + + iteratee, err := sdb.Iteratee(root) + if err != nil { + t.Fatalf("(%s) failed to create iteratee: %v", scheme, err) + } + // Build lookups from address hash. + addrByHash := make(map[common.Hash]*testAccount) + for _, acc := range accounts { + addrByHash[crypto.Keccak256Hash(acc.address.Bytes())] = acc + } + + // --- Full iteration: count, ordering, RLP validity, data integrity, address resolution --- + acctIt, err := iteratee.NewAccountIterator(common.Hash{}) + if err != nil { + t.Fatalf("(%s) failed to create account iterator: %v", scheme, err) + } + var ( + hashes []common.Hash + prevHash common.Hash + ) + for acctIt.Next() { + hash := acctIt.Hash() + if hash == (common.Hash{}) { + t.Fatalf("(%s) zero hash at position %d", scheme, len(hashes)) + } + if len(hashes) > 0 && bytes.Compare(prevHash.Bytes(), hash.Bytes()) >= 0 { + t.Fatalf("(%s) hashes not ascending: %x >= %x", scheme, prevHash, hash) + } + prevHash = hash + hashes = append(hashes, hash) + + // Decode and verify account data. + blob := acctIt.Account() + if blob == nil { + t.Fatalf("(%s) nil account at %x", scheme, hash) + } + var decoded types.StateAccount + if err := rlp.DecodeBytes(blob, &decoded); err != nil { + t.Fatalf("(%s) bad RLP at %x: %v", scheme, hash, err) + } + acc := addrByHash[hash] + if decoded.Nonce != acc.nonce { + t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, decoded.Nonce, acc.nonce) + } + if decoded.Balance.Cmp(acc.balance) != 0 { + t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, decoded.Balance, acc.balance) + } + // Verify address preimage resolution. + addr, err := acctIt.Address() + if err != nil { + t.Fatalf("(%s) failed to address: %v", scheme, err) + } + if addr != acc.address { + t.Fatalf("(%s) Address() = %x, want %x", scheme, addr, acc.address) + } + } + acctIt.Release() + + if err := acctIt.Error(); err != nil { + t.Fatalf("(%s) iteration error: %v", scheme, err) + } + if len(hashes) != len(accounts) { + t.Fatalf("(%s) iterated %d accounts, want %d", scheme, len(hashes), len(accounts)) + } + + // --- Seek: starting from midpoint should skip earlier entries --- + mid := hashes[len(hashes)/2] + seekIt, err := iteratee.NewAccountIterator(mid) + if err != nil { + t.Fatalf("(%s) failed to create seeked iterator: %v", scheme, err) + } + seekCount := 0 + for seekIt.Next() { + if bytes.Compare(seekIt.Hash().Bytes(), mid.Bytes()) < 0 { + t.Fatalf("(%s) seeked iterator returned hash before start", scheme) + } + seekCount++ + } + seekIt.Release() + + if seekCount != len(hashes)/2 { + t.Fatalf("(%s) unexpected seeked count, %d != %d", scheme, seekCount, len(hashes)/2) + } +} + +// TestStorageIterator tests the storage iterator: correct slot counts against +// the trie, ascending hash order, non-nil slot data, key preimage resolution, +// seek behavior, and empty-storage accounts. +func TestStorageIterator(t *testing.T) { + testStorageIterator(t, rawdb.HashScheme) + testStorageIterator(t, rawdb.PathScheme) +} + +func testStorageIterator(t *testing.T, scheme string) { + _, sdb, ndb, root, accounts := makeTestState(scheme) + ndb.Commit(root, false) + + iteratee, err := sdb.Iteratee(root) + if err != nil { + t.Fatalf("(%s) failed to create iteratee: %v", scheme, err) + } + + // --- Slot count and ordering for every account --- + var withStorage common.Hash // remember an account that has storage for seek test + for _, acc := range accounts { + addrHash := crypto.Keccak256Hash(acc.address.Bytes()) + expected := countStorageSlots(t, scheme, sdb, root, addrHash) + + storageIt, err := iteratee.NewStorageIterator(addrHash, common.Hash{}) + if err != nil { + t.Fatalf("(%s) failed to create storage iterator for %x: %v", scheme, acc.address, err) + } + count := 0 + var prevHash common.Hash + for storageIt.Next() { + hash := storageIt.Hash() + if count > 0 && bytes.Compare(prevHash.Bytes(), hash.Bytes()) >= 0 { + t.Fatalf("(%s) storage hashes not ascending for %x", scheme, acc.address) + } + prevHash = hash + if storageIt.Slot() == nil { + t.Fatalf("(%s) nil slot at %x", scheme, hash) + } + // Check key preimage resolution on first slot. + if _, err := storageIt.Key(); err != nil { + t.Fatalf("(%s) Key() failed to resolve", scheme) + } + count++ + } + if err := storageIt.Error(); err != nil { + t.Fatalf("(%s) storage iteration error for %x: %v", scheme, acc.address, err) + } + storageIt.Release() + + if count != expected { + t.Fatalf("(%s) account %x: %d slots, want %d", scheme, acc.address, count, expected) + } + if count > 0 { + withStorage = addrHash + } + } + + // --- Seek: starting from second slot should skip the first --- + if withStorage == (common.Hash{}) { + t.Fatalf("(%s) no account with storage found", scheme) + } + fullIt, err := iteratee.NewStorageIterator(withStorage, common.Hash{}) + if err != nil { + t.Fatalf("(%s) failed to create full storage iterator: %v", scheme, err) + } + var slotHashes []common.Hash + for fullIt.Next() { + slotHashes = append(slotHashes, fullIt.Hash()) + } + fullIt.Release() + + seekIt, err := iteratee.NewStorageIterator(withStorage, slotHashes[1]) + if err != nil { + t.Fatalf("(%s) failed to create seeked storage iterator: %v", scheme, err) + } + seekCount := 0 + for seekIt.Next() { + if bytes.Compare(seekIt.Hash().Bytes(), slotHashes[1].Bytes()) < 0 { + t.Fatalf("(%s) seeked storage iterator returned hash before start", scheme) + } + seekCount++ + } + seekIt.Release() + + if seekCount != len(slotHashes)-1 { + t.Fatalf("(%s) unexpected seeked storage count %d != %d", scheme, seekCount, len(slotHashes)-1) + } +} + +// countStorageSlots counts storage slots for an account by opening the +// storage trie directly. +func countStorageSlots(t *testing.T, scheme string, sdb Database, root common.Hash, addrHash common.Hash) int { + t.Helper() + accTrie, err := trie.NewStateTrie(trie.StateTrieID(root), sdb.TrieDB()) + if err != nil { + t.Fatalf("(%s) failed to open account trie: %v", scheme, err) + } + acct, err := accTrie.GetAccountByHash(addrHash) + if err != nil || acct == nil || acct.Root == types.EmptyRootHash { + return 0 + } + storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acct.Root), sdb.TrieDB()) + if err != nil { + t.Fatalf("(%s) failed to open storage trie for %x: %v", scheme, addrHash, err) + } + it := trie.NewIterator(storageTrie.MustNodeIterator(nil)) + count := 0 + for it.Next() { + count++ + } + return count +} diff --git a/core/state/dump.go b/core/state/dump.go index 829d106ed3..71138143d9 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -27,7 +27,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" ) @@ -45,6 +44,7 @@ type DumpConfig struct { type DumpCollector interface { // OnRoot is called with the state root OnRoot(common.Hash) + // OnAccount is called once for each account in the trie OnAccount(*common.Address, DumpAccount) } @@ -65,9 +65,10 @@ type DumpAccount struct { type Dump struct { Root string `json:"root"` Accounts map[string]DumpAccount `json:"accounts"` + // Next can be set to represent that this dump is only partial, and Next // is where an iterator should be positioned in order to continue the dump. - Next []byte `json:"next,omitempty"` // nil if no more accounts + Next hexutil.Bytes `json:"next,omitempty"` // nil if no more accounts } // OnRoot implements DumpCollector interface @@ -114,9 +115,6 @@ func (d iterativeDump) OnRoot(root common.Hash) { // DumpToCollector iterates the state according to the given options and inserts // the items into a collector for aggregation or serialization. -// -// The state iterator is still trie-based and can be converted to snapshot-based -// once the state snapshot is fully integrated into database. TODO(rjl493456442). func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) { // Sanitize the input to allow nil configs if conf == nil { @@ -131,20 +129,23 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] log.Info("Trie dumping started", "root", s.originalRoot) c.OnRoot(s.originalRoot) - tr, err := s.db.OpenTrie(s.originalRoot) + iteratee, err := s.db.Iteratee(s.originalRoot) if err != nil { return nil } - trieIt, err := tr.NodeIterator(conf.Start) + var startHash common.Hash + if conf.Start != nil { + startHash = common.BytesToHash(conf.Start) + } + acctIt, err := iteratee.NewAccountIterator(startHash) if err != nil { - log.Error("Trie dumping error", "err", err) return nil } - it := trie.NewIterator(trieIt) + defer acctIt.Release() - for it.Next() { + for acctIt.Next() { var data types.StateAccount - if err := rlp.DecodeBytes(it.Value, &data); err != nil { + if err := rlp.DecodeBytes(acctIt.Account(), &data); err != nil { panic(err) } var ( @@ -153,63 +154,55 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] Nonce: data.Nonce, Root: data.Root[:], CodeHash: data.CodeHash, - AddressHash: it.Key, + AddressHash: acctIt.Hash().Bytes(), } - address *common.Address - addr common.Address - addrBytes = tr.GetKey(it.Key) + address *common.Address ) - if addrBytes == nil { + addrBytes, err := acctIt.Address() + if err != nil { missingPreimages++ if conf.OnlyWithAddresses { continue } } else { - addr = common.BytesToAddress(addrBytes) - address = &addr + address = &addrBytes account.Address = address } - obj := newObject(s, addr, &data) + obj := newObject(s, addrBytes, &data) if !conf.SkipCode { account.Code = obj.Code() } if !conf.SkipStorage { account.Storage = make(map[common.Hash]string) - storageTr, err := s.db.OpenStorageTrie(s.originalRoot, addr, obj.Root(), tr) + storageIt, err := iteratee.NewStorageIterator(acctIt.Hash(), common.Hash{}) if err != nil { log.Error("Failed to load storage trie", "err", err) continue } - trieIt, err := storageTr.NodeIterator(nil) - if err != nil { - log.Error("Failed to create trie iterator", "err", err) - continue - } - storageIt := trie.NewIterator(trieIt) for storageIt.Next() { - _, content, _, err := rlp.Split(storageIt.Value) + _, content, _, err := rlp.Split(storageIt.Slot()) if err != nil { log.Error("Failed to decode the value returned by iterator", "error", err) continue } - key := storageTr.GetKey(storageIt.Key) - if key == nil { + key, err := storageIt.Key() + if err != nil { continue } - account.Storage[common.BytesToHash(key)] = common.Bytes2Hex(content) + account.Storage[key] = common.Bytes2Hex(content) } + storageIt.Release() } c.OnAccount(address, account) accounts++ if time.Since(logged) > 8*time.Second { - log.Info("Trie dumping in progress", "at", common.Bytes2Hex(it.Key), "accounts", accounts, - "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Trie dumping in progress", "at", acctIt.Hash().Hex(), "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start))) logged = time.Now() } if conf.Max > 0 && accounts >= conf.Max { - if it.Next() { - nextKey = it.Key + if acctIt.Next() { + nextKey = acctIt.Hash().Bytes() } break } @@ -217,9 +210,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] if missingPreimages > 0 { log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages) } - log.Info("Trie dumping complete", "accounts", accounts, - "elapsed", common.PrettyDuration(time.Since(start))) - + log.Info("Trie dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start))) return nextKey } diff --git a/core/state/statedb.go b/core/state/statedb.go index 769c8504c2..854aaf6109 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -28,7 +28,6 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" @@ -1039,31 +1038,32 @@ func (s *StateDB) clearJournalAndRefund() { s.refund = 0 } -// fastDeleteStorage is the function that efficiently deletes the storage trie -// of a specific account. It leverages the associated state snapshot for fast -// storage iteration and constructs trie node deletion markers by creating -// stack trie with iterated slots. -func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { - iter, err := snaps.StorageIterator(s.originalRoot, addrHash, common.Hash{}) - if err != nil { - return nil, nil, nil, err - } - defer iter.Release() - +// deleteStorage is designed to delete the storage trie of a designated account. +func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { var ( nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil) storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot ) + iteratee, err := s.db.Iteratee(s.originalRoot) + if err != nil { + return nil, nil, nil, err + } + it, err := iteratee.NewStorageIterator(addrHash, common.Hash{}) + if err != nil { + return nil, nil, nil, err + } + defer it.Release() + stack := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) { nodes.AddNode(path, trienode.NewDeletedWithPrev(blob)) }) - for iter.Next() { - slot := common.CopyBytes(iter.Slot()) - if err := iter.Error(); err != nil { // error might occur after Slot function + for it.Next() { + slot := common.CopyBytes(it.Slot()) + if err := it.Error(); err != nil { // error might occur after Slot function return nil, nil, nil, err } - key := iter.Hash() + key := it.Hash() storages[key] = nil storageOrigins[key] = slot @@ -1071,7 +1071,7 @@ func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash, return nil, nil, nil, err } } - if err := iter.Error(); err != nil { // error might occur during iteration + if err := it.Error(); err != nil { // error might occur during iteration return nil, nil, nil, err } if stack.Hash() != root { @@ -1080,68 +1080,6 @@ func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash, return storages, storageOrigins, nodes, nil } -// slowDeleteStorage serves as a less-efficient alternative to "fastDeleteStorage," -// employed when the associated state snapshot is not available. It iterates the -// storage slots along with all internal trie nodes via trie directly. -func (s *StateDB) slowDeleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { - tr, err := s.db.OpenStorageTrie(s.originalRoot, addr, root, s.trie) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to open storage trie, err: %w", err) - } - it, err := tr.NodeIterator(nil) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to open storage iterator, err: %w", err) - } - var ( - nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) - storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil) - storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot - ) - for it.Next(true) { - if it.Leaf() { - key := common.BytesToHash(it.LeafKey()) - storages[key] = nil - storageOrigins[key] = common.CopyBytes(it.LeafBlob()) - continue - } - if it.Hash() == (common.Hash{}) { - continue - } - nodes.AddNode(it.Path(), trienode.NewDeletedWithPrev(it.NodeBlob())) - } - if err := it.Error(); err != nil { - return nil, nil, nil, err - } - return storages, storageOrigins, nodes, nil -} - -// deleteStorage is designed to delete the storage trie of a designated account. -// The function will make an attempt to utilize an efficient strategy if the -// associated state snapshot is reachable; otherwise, it will resort to a less -// efficient approach. -func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { - var ( - err error - nodes *trienode.NodeSet // the set for trie node mutations (value is nil) - storages map[common.Hash][]byte // the set for storage mutations (value is nil) - storageOrigins map[common.Hash][]byte // the set for tracking the original value of slot - ) - // The fast approach can be failed if the snapshot is not fully - // generated, or it's internally corrupted. Fallback to the slow - // one just in case. - snaps := s.db.Snapshot() - if snaps != nil { - storages, storageOrigins, nodes, err = s.fastDeleteStorage(snaps, addrHash, root) - } - if snaps == nil || err != nil { - storages, storageOrigins, nodes, err = s.slowDeleteStorage(addr, addrHash, root) - } - if err != nil { - return nil, nil, nil, err - } - return storages, storageOrigins, nodes, nil -} - // handleDestruction processes all destruction markers and deletes the account // and associated storage slots if necessary. There are four potential scenarios // as following: @@ -1192,7 +1130,7 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr) } // Remove storage slots belonging to the account. - storages, storagesOrigin, set, err := s.deleteStorage(addr, addrHash, prev.Root) + storages, storagesOrigin, set, err := s.deleteStorage(addrHash, prev.Root) if err != nil { return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err) } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 8d1f93ca1b..d29b262eea 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -1296,12 +1296,12 @@ func TestDeleteStorage(t *testing.T) { obj := fastState.getOrNewStateObject(addr) storageRoot := obj.data.Root - _, _, fastNodes, err := fastState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot) + _, _, fastNodes, err := fastState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot) if err != nil { t.Fatal(err) } - _, _, slowNodes, err := slowState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot) + _, _, slowNodes, err := slowState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot) if err != nil { t.Fatal(err) } diff --git a/eth/api_debug.go b/eth/api_debug.go index 988eb44216..5dd535e672 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -236,6 +236,8 @@ func storageRangeAt(statedb *state.StateDB, root common.Hash, address common.Add if storageRoot == types.EmptyRootHash || storageRoot == (common.Hash{}) { return StorageRangeResult{}, nil // empty storage } + // TODO(rjl493456442) it's problematic for traversing the state with in-memory + // state mutations, specifically txIndex != 0. id := trie.StorageTrieID(root, crypto.Keccak256Hash(address.Bytes()), storageRoot) tr, err := trie.NewStateTrie(id, statedb.Database().TrieDB()) if err != nil { From 00da4f51fffbbfaf2a9b962ba2324629d5f0c45f Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:10:32 -0500 Subject: [PATCH 09/27] core, eth/protocols/snap: Snap/2 Protocol + BAL Serving (#34083) Implement the snap/2 wire protocol with BAL serving --------- Co-authored-by: Gary Rong --- core/blockchain_reader.go | 8 + eth/downloader/downloader_test.go | 2 +- eth/protocols/eth/handler.go | 1 - eth/protocols/snap/handler.go | 571 ++------------------ eth/protocols/snap/handler_fuzzing_test.go | 6 + eth/protocols/snap/handler_test.go | 195 +++++++ eth/protocols/snap/handlers.go | 593 +++++++++++++++++++++ eth/protocols/snap/protocol.go | 25 +- 8 files changed, 874 insertions(+), 527 deletions(-) create mode 100644 eth/protocols/snap/handler_test.go create mode 100644 eth/protocols/snap/handlers.go diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index f1b40d0d0c..8b026680d2 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -296,6 +296,14 @@ func (bc *BlockChain) GetReceiptsRLP(hash common.Hash) rlp.RawValue { return rawdb.ReadReceiptsRLP(bc.db, hash, number) } +func (bc *BlockChain) GetAccessListRLP(hash common.Hash) rlp.RawValue { + number, ok := rawdb.ReadHeaderNumber(bc.db, hash) + if !ok { + return nil + } + return rawdb.ReadAccessListRLP(bc.db, hash, number) +} + // GetUnclesInChain retrieves all the uncles from a given block backwards until // a specific distance is reached. func (bc *BlockChain) GetUnclesInChain(block *types.Block, length int) []*types.Header { diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 01a994dbfd..9280d455fb 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -370,7 +370,7 @@ func (dlp *downloadTesterPeer) RequestTrieNodes(id uint64, root common.Hash, cou Paths: encPaths, Bytes: uint64(bytes), } - nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req, time.Now()) + nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req) go dlp.dl.downloader.SnapSyncer.OnTrieNodes(dlp, id, nodes) return nil } diff --git a/eth/protocols/eth/handler.go b/eth/protocols/eth/handler.go index 59512f5be7..f7d25bd8ca 100644 --- a/eth/protocols/eth/handler.go +++ b/eth/protocols/eth/handler.go @@ -167,7 +167,6 @@ func Handle(backend Backend, peer *Peer) error { type msgHandler func(backend Backend, msg Decoder, peer *Peer) error type Decoder interface { Decode(val interface{}) error - Time() time.Time } var eth69 = map[uint64]msgHandler{ diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go index 071a0419fb..26545f2960 100644 --- a/eth/protocols/snap/handler.go +++ b/eth/protocols/snap/handler.go @@ -17,25 +17,14 @@ package snap import ( - "bytes" "fmt" "time" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state/snapshot" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/tracker" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" - "github.com/ethereum/go-ethereum/triedb/database" ) const ( @@ -55,6 +44,10 @@ const ( // number is there to limit the number of disk lookups. maxTrieNodeLookups = 1024 + // maxAccessListLookups is the maximum number of BALs to server. This number + // is there to limit the number of disk lookups. + maxAccessListLookups = 1024 + // maxTrieNodeTimeSpent is the maximum time we should spend on looking up trie nodes. // If we spend too much time, then it's a fairly high chance of timing out // at the remote side, which means all the work is in vain. @@ -123,6 +116,34 @@ func Handle(backend Backend, peer *Peer) error { } } +type msgHandler func(backend Backend, msg Decoder, peer *Peer) error +type Decoder interface { + Decode(val interface{}) error +} + +var snap1 = map[uint64]msgHandler{ + GetAccountRangeMsg: handleGetAccountRange, + AccountRangeMsg: handleAccountRange, + GetStorageRangesMsg: handleGetStorageRanges, + StorageRangesMsg: handleStorageRanges, + GetByteCodesMsg: handleGetByteCodes, + ByteCodesMsg: handleByteCodes, + GetTrieNodesMsg: handleGetTrienodes, + TrieNodesMsg: handleTrieNodes, +} + +// nolint:unused +var snap2 = map[uint64]msgHandler{ + GetAccountRangeMsg: handleGetAccountRange, + AccountRangeMsg: handleAccountRange, + GetStorageRangesMsg: handleGetStorageRanges, + StorageRangesMsg: handleStorageRanges, + GetByteCodesMsg: handleGetByteCodes, + ByteCodesMsg: handleByteCodes, + GetAccessListsMsg: handleGetAccessLists, + // AccessListsMsg: TODO +} + // HandleMessage is invoked whenever an inbound message is received from a // remote peer on the `snap` protocol. The remote connection is torn down upon // returning any error. @@ -136,8 +157,19 @@ func HandleMessage(backend Backend, peer *Peer) error { return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) } defer msg.Discard() - start := time.Now() + + var handlers map[uint64]msgHandler + switch peer.version { + case SNAP1: + handlers = snap1 + //case SNAP2: + // handlers = snap2 + default: + return fmt.Errorf("unknown eth protocol version: %v", peer.version) + } + // Track the amount of time it takes to serve the request and run the handler + start := time.Now() if metrics.Enabled() { h := fmt.Sprintf("%s/%s/%d/%#02x", p2p.HandleHistName, ProtocolName, peer.Version(), msg.Code) defer func(start time.Time) { @@ -149,520 +181,11 @@ func HandleMessage(backend Backend, peer *Peer) error { metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(start).Microseconds()) }(start) } - // Handle the message depending on its contents - switch { - case msg.Code == GetAccountRangeMsg: - var req GetAccountRangePacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req) - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{ - ID: req.ID, - Accounts: accounts, - Proof: proofs, - }) - - case msg.Code == AccountRangeMsg: - res := new(accountRangeInput) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - - // Check response validity. - if len := res.Proof.Len(); len > 128 { - return fmt.Errorf("AccountRange: invalid proof (length %d)", len) - } - tresp := tracker.Response{ID: res.ID, MsgCode: AccountRangeMsg, Size: len(res.Accounts.Content())} - if err := peer.tracker.Fulfil(tresp); err != nil { - return err - } - - // Decode. - accounts, err := res.Accounts.Items() - if err != nil { - return fmt.Errorf("AccountRange: invalid accounts list: %v", err) - } - proof, err := res.Proof.Items() - if err != nil { - return fmt.Errorf("AccountRange: invalid proof: %v", err) - } - - // Ensure the range is monotonically increasing - for i := 1; i < len(accounts); i++ { - if bytes.Compare(accounts[i-1].Hash[:], accounts[i].Hash[:]) >= 0 { - return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, accounts[i-1].Hash[:], i, accounts[i].Hash[:]) - } - } - - return backend.Handle(peer, &AccountRangePacket{res.ID, accounts, proof}) - - case msg.Code == GetStorageRangesMsg: - var req GetStorageRangesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req) - - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{ - ID: req.ID, - Slots: slots, - Proof: proofs, - }) - - case msg.Code == StorageRangesMsg: - res := new(storageRangesInput) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - - // Check response validity. - if len := res.Proof.Len(); len > 128 { - return fmt.Errorf("StorageRangesMsg: invalid proof (length %d)", len) - } - tresp := tracker.Response{ID: res.ID, MsgCode: StorageRangesMsg, Size: len(res.Slots.Content())} - if err := peer.tracker.Fulfil(tresp); err != nil { - return fmt.Errorf("StorageRangesMsg: %w", err) - } - - // Decode. - slotLists, err := res.Slots.Items() - if err != nil { - return fmt.Errorf("AccountRange: invalid accounts list: %v", err) - } - proof, err := res.Proof.Items() - if err != nil { - return fmt.Errorf("AccountRange: invalid proof: %v", err) - } - - // Ensure the ranges are monotonically increasing - for i, slots := range slotLists { - for j := 1; j < len(slots); j++ { - if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 { - return fmt.Errorf("storage slots not monotonically increasing for account #%d: #%d [%x] vs #%d [%x]", i, j-1, slots[j-1].Hash[:], j, slots[j].Hash[:]) - } - } - } - - return backend.Handle(peer, &StorageRangesPacket{res.ID, slotLists, proof}) - - case msg.Code == GetByteCodesMsg: - var req GetByteCodesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - codes := ServiceGetByteCodesQuery(backend.Chain(), &req) - - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{ - ID: req.ID, - Codes: codes, - }) - - case msg.Code == ByteCodesMsg: - res := new(byteCodesInput) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - - length := res.Codes.Len() - tresp := tracker.Response{ID: res.ID, MsgCode: ByteCodesMsg, Size: length} - if err := peer.tracker.Fulfil(tresp); err != nil { - return fmt.Errorf("ByteCodes: %w", err) - } - - codes, err := res.Codes.Items() - if err != nil { - return fmt.Errorf("ByteCodes: %w", err) - } - - return backend.Handle(peer, &ByteCodesPacket{res.ID, codes}) - - case msg.Code == GetTrieNodesMsg: - var req GetTrieNodesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req, start) - if err != nil { - return err - } - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{ - ID: req.ID, - Nodes: nodes, - }) - - case msg.Code == TrieNodesMsg: - res := new(trieNodesInput) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - - tresp := tracker.Response{ID: res.ID, MsgCode: TrieNodesMsg, Size: res.Nodes.Len()} - if err := peer.tracker.Fulfil(tresp); err != nil { - return fmt.Errorf("TrieNodes: %w", err) - } - nodes, err := res.Nodes.Items() - if err != nil { - return fmt.Errorf("TrieNodes: %w", err) - } - - return backend.Handle(peer, &TrieNodesPacket{res.ID, nodes}) - - default: - return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) + if handler := handlers[msg.Code]; handler != nil { + return handler(backend, msg, peer) } -} - -// ServiceGetAccountRangeQuery assembles the response to an account range query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // Retrieve the requested state and bail out if non existent - tr, err := trie.New(trie.StateTrieID(req.Root), chain.TrieDB()) - if err != nil { - return nil, nil - } - // Temporary solution: using the snapshot interface for both cases. - // This can be removed once the hash scheme is deprecated. - var it snapshot.AccountIterator - if chain.TrieDB().Scheme() == rawdb.HashScheme { - // The snapshot is assumed to be available in hash mode if - // the SNAP protocol is enabled. - it, err = chain.Snapshots().AccountIterator(req.Root, req.Origin) - } else { - it, err = chain.TrieDB().AccountIterator(req.Root, req.Origin) - } - if err != nil { - return nil, nil - } - // Iterate over the requested range and pile accounts up - var ( - accounts []*AccountData - size uint64 - last common.Hash - ) - for it.Next() { - hash, account := it.Hash(), common.CopyBytes(it.Account()) - - // Track the returned interval for the Merkle proofs - last = hash - - // Assemble the reply item - size += uint64(common.HashLength + len(account)) - accounts = append(accounts, &AccountData{ - Hash: hash, - Body: account, - }) - // If we've exceeded the request threshold, abort - if bytes.Compare(hash[:], req.Limit[:]) >= 0 { - break - } - if size > req.Bytes { - break - } - } - it.Release() - - // Generate the Merkle proofs for the first and last account - proof := trienode.NewProofSet() - if err := tr.Prove(req.Origin[:], proof); err != nil { - log.Warn("Failed to prove account range", "origin", req.Origin, "err", err) - return nil, nil - } - if last != (common.Hash{}) { - if err := tr.Prove(last[:], proof); err != nil { - log.Warn("Failed to prove account range", "last", last, "err", err) - return nil, nil - } - } - return accounts, proof.List() -} - -func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set? - // TODO(karalabe): - Logging locally is not ideal as remote faults annoy the local user - // TODO(karalabe): - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional) - - // Calculate the hard limit at which to abort, even if mid storage trie - hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack)) - - // Retrieve storage ranges until the packet limit is reached - var ( - slots [][]*StorageData - proofs [][]byte - size uint64 - ) - for _, account := range req.Accounts { - // If we've exceeded the requested data limit, abort without opening - // a new storage range (that we'd need to prove due to exceeded size) - if size >= req.Bytes { - break - } - // The first account might start from a different origin and end sooner - var origin common.Hash - if len(req.Origin) > 0 { - origin, req.Origin = common.BytesToHash(req.Origin), nil - } - var limit = common.MaxHash - if len(req.Limit) > 0 { - limit, req.Limit = common.BytesToHash(req.Limit), nil - } - // Retrieve the requested state and bail out if non existent - var ( - err error - it snapshot.StorageIterator - ) - // Temporary solution: using the snapshot interface for both cases. - // This can be removed once the hash scheme is deprecated. - if chain.TrieDB().Scheme() == rawdb.HashScheme { - // The snapshot is assumed to be available in hash mode if - // the SNAP protocol is enabled. - it, err = chain.Snapshots().StorageIterator(req.Root, account, origin) - } else { - it, err = chain.TrieDB().StorageIterator(req.Root, account, origin) - } - if err != nil { - return nil, nil - } - // Iterate over the requested range and pile slots up - var ( - storage []*StorageData - last common.Hash - abort bool - ) - for it.Next() { - if size >= hardLimit { - abort = true - break - } - hash, slot := it.Hash(), common.CopyBytes(it.Slot()) - - // Track the returned interval for the Merkle proofs - last = hash - - // Assemble the reply item - size += uint64(common.HashLength + len(slot)) - storage = append(storage, &StorageData{ - Hash: hash, - Body: slot, - }) - // If we've exceeded the request threshold, abort - if bytes.Compare(hash[:], limit[:]) >= 0 { - break - } - } - if len(storage) > 0 { - slots = append(slots, storage) - } - it.Release() - - // Generate the Merkle proofs for the first and last storage slot, but - // only if the response was capped. If the entire storage trie included - // in the response, no need for any proofs. - if origin != (common.Hash{}) || (abort && len(storage) > 0) { - // Request started at a non-zero hash or was capped prematurely, add - // the endpoint Merkle proofs - accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.TrieDB()) - if err != nil { - return nil, nil - } - acc, err := accTrie.GetAccountByHash(account) - if err != nil || acc == nil { - return nil, nil - } - id := trie.StorageTrieID(req.Root, account, acc.Root) - stTrie, err := trie.NewStateTrie(id, chain.TrieDB()) - if err != nil { - return nil, nil - } - proof := trienode.NewProofSet() - if err := stTrie.Prove(origin[:], proof); err != nil { - log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err) - return nil, nil - } - if last != (common.Hash{}) { - if err := stTrie.Prove(last[:], proof); err != nil { - log.Warn("Failed to prove storage range", "last", last, "err", err) - return nil, nil - } - } - proofs = append(proofs, proof.List()...) - // Proof terminates the reply as proofs are only added if a node - // refuses to serve more data (exception when a contract fetch is - // finishing, but that's that). - break - } - } - return slots, proofs -} - -// ServiceGetByteCodesQuery assembles the response to a byte codes query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - if len(req.Hashes) > maxCodeLookups { - req.Hashes = req.Hashes[:maxCodeLookups] - } - // Retrieve bytecodes until the packet size limit is reached - var ( - codes [][]byte - bytes uint64 - ) - for _, hash := range req.Hashes { - if hash == types.EmptyCodeHash { - // Peers should not request the empty code, but if they do, at - // least sent them back a correct response without db lookups - codes = append(codes, []byte{}) - } else if blob := chain.ContractCodeWithPrefix(hash); len(blob) > 0 { - codes = append(codes, blob) - bytes += uint64(len(blob)) - } - if bytes > req.Bytes { - break - } - } - return codes -} - -// ServiceGetTrieNodesQuery assembles the response to a trie nodes query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket, start time.Time) ([][]byte, error) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // Make sure we have the state associated with the request - triedb := chain.TrieDB() - - accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb) - if err != nil { - // We don't have the requested state available, bail out - return nil, nil - } - // The 'reader' might be nil, in which case we cannot serve storage slots - // via snapshot. - var reader database.StateReader - if chain.Snapshots() != nil { - reader = chain.Snapshots().Snapshot(req.Root) - } - if reader == nil { - reader, _ = triedb.StateReader(req.Root) - } - - // Retrieve trie nodes until the packet size limit is reached - var ( - outerIt = req.Paths.ContentIterator() - nodes [][]byte - bytes uint64 - loads int // Trie hash expansions to count database reads - ) - for outerIt.Next() { - innerIt, err := rlp.NewListIterator(outerIt.Value()) - if err != nil { - return nodes, err - } - - switch innerIt.Count() { - case 0: - // Ensure we penalize invalid requests - return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest) - - case 1: - // If we're only retrieving an account trie node, fetch it directly - accKey := nextBytes(&innerIt) - if accKey == nil { - return nodes, fmt.Errorf("%w: invalid account node request", errBadRequest) - } - blob, resolved, err := accTrie.GetNode(accKey) - loads += resolved // always account database reads, even for failures - if err != nil { - break - } - nodes = append(nodes, blob) - bytes += uint64(len(blob)) - - default: - // Storage slots requested, open the storage trie and retrieve from there - accKey := nextBytes(&innerIt) - if accKey == nil { - return nodes, fmt.Errorf("%w: invalid account storage request", errBadRequest) - } - var stRoot common.Hash - if reader == nil { - // We don't have the requested state snapshotted yet (or it is stale), - // but can look up the account via the trie instead. - account, err := accTrie.GetAccountByHash(common.BytesToHash(accKey)) - loads += 8 // We don't know the exact cost of lookup, this is an estimate - if err != nil || account == nil { - break - } - stRoot = account.Root - } else { - account, err := reader.Account(common.BytesToHash(accKey)) - loads++ // always account database reads, even for failures - if err != nil || account == nil { - break - } - stRoot = common.BytesToHash(account.Root) - } - - id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot) - stTrie, err := trie.NewStateTrie(id, triedb) - loads++ // always account database reads, even for failures - if err != nil { - break - } - for innerIt.Next() { - path, _, err := rlp.SplitString(innerIt.Value()) - if err != nil { - return nil, fmt.Errorf("%w: invalid storage key: %v", errBadRequest, err) - } - blob, resolved, err := stTrie.GetNode(path) - loads += resolved // always account database reads, even for failures - if err != nil { - break - } - nodes = append(nodes, blob) - bytes += uint64(len(blob)) - - // Sanity check limits to avoid DoS on the store trie loads - if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { - break - } - } - } - // Abort request processing if we've exceeded our limits - if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { - break - } - } - return nodes, nil -} - -func nextBytes(it *rlp.Iterator) []byte { - if !it.Next() { - return nil - } - content, _, err := rlp.SplitString(it.Value()) - if err != nil { - return nil - } - return content + return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) } // NodeInfo represents a short summary of the `snap` sub-protocol metadata diff --git a/eth/protocols/snap/handler_fuzzing_test.go b/eth/protocols/snap/handler_fuzzing_test.go index 4930ae9ae6..a52da0aac5 100644 --- a/eth/protocols/snap/handler_fuzzing_test.go +++ b/eth/protocols/snap/handler_fuzzing_test.go @@ -60,6 +60,12 @@ func FuzzTrieNodes(f *testing.F) { }) } +func FuzzAccessLists(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + doFuzz(data, &GetAccessListsPacket{}, GetAccessListsMsg) + }) +} + func doFuzz(input []byte, obj interface{}, code int) { bc := getChain() defer bc.Stop() diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go new file mode 100644 index 0000000000..cb4b378a8d --- /dev/null +++ b/eth/protocols/snap/handler_test.go @@ -0,0 +1,195 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snap + +import ( + "bytes" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" +) + +// getChainWithBALs creates a minimal test chain with BALs stored for each block. +// It returns the chain, block hashes, and the stored BAL data. +func getChainWithBALs(nBlocks int, balSize int) (*core.BlockChain, []common.Hash, []rlp.RawValue) { + gspec := &core.Genesis{ + Config: params.TestChainConfig, + } + db := rawdb.NewMemoryDatabase() + _, blocks, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), nBlocks, func(i int, gen *core.BlockGen) {}) + options := &core.BlockChainConfig{ + TrieCleanLimit: 0, + TrieDirtyLimit: 0, + TrieTimeLimit: 5 * time.Minute, + NoPrefetch: true, + SnapshotLimit: 0, + } + bc, err := core.NewBlockChain(db, gspec, ethash.NewFaker(), options) + if err != nil { + panic(err) + } + if _, err := bc.InsertChain(blocks); err != nil { + panic(err) + } + + // Store BALs for each block + var hashes []common.Hash + var bals []rlp.RawValue + for _, block := range blocks { + hash := block.Hash() + number := block.NumberU64() + bal := make(rlp.RawValue, balSize) + + // Fill with data based on block number + for j := range bal { + bal[j] = byte(number + uint64(j)) + } + rawdb.WriteAccessListRLP(db, hash, number, bal) + hashes = append(hashes, hash) + bals = append(bals, bal) + } + return bc, hashes, bals +} + +// TestServiceGetAccessListsQuery verifies that known block hashes return the +// correct BALs with positional correspondence. +func TestServiceGetAccessListsQuery(t *testing.T) { + t.Parallel() + bc, hashes, bals := getChainWithBALs(5, 100) + defer bc.Stop() + req := &GetAccessListsPacket{ + ID: 1, + Hashes: hashes, + } + result := ServiceGetAccessListsQuery(bc, req) + + // Verify the results + if len(result) != len(hashes) { + t.Fatalf("expected %d results, got %d", len(hashes), len(result)) + } + for i, bal := range result { + if !bytes.Equal(bal, bals[i]) { + t.Errorf("BAL %d mismatch: got %x, want %x", i, bal, bals[i]) + } + } +} + +// TestServiceGetAccessListsQueryEmpty verifies that unknown block hashes return +// nil placeholders and that mixed known/unknown hashes preserve alignment. +func TestServiceGetAccessListsQueryEmpty(t *testing.T) { + t.Parallel() + bc, hashes, bals := getChainWithBALs(3, 100) + defer bc.Stop() + unknown := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + mixed := []common.Hash{hashes[0], unknown, hashes[1], unknown, hashes[2]} + req := &GetAccessListsPacket{ + ID: 2, + Hashes: mixed, + } + result := ServiceGetAccessListsQuery(bc, req) + + // Verify length + if len(result) != len(mixed) { + t.Fatalf("expected %d results, got %d", len(mixed), len(result)) + } + + // Check positional correspondence + if !bytes.Equal(result[0], bals[0]) { + t.Errorf("index 0: expected known BAL, got %x", result[0]) + } + if result[1] != nil { + t.Errorf("index 1: expected nil for unknown hash, got %x", result[1]) + } + if !bytes.Equal(result[2], bals[1]) { + t.Errorf("index 2: expected known BAL, got %x", result[2]) + } + if result[3] != nil { + t.Errorf("index 3: expected nil for unknown hash, got %x", result[3]) + } + if !bytes.Equal(result[4], bals[2]) { + t.Errorf("index 4: expected known BAL, got %x", result[4]) + } +} + +// TestServiceGetAccessListsQueryCap verifies that requests exceeding +// maxAccessListLookups are capped. +func TestServiceGetAccessListsQueryCap(t *testing.T) { + t.Parallel() + + bc, _, _ := getChainWithBALs(2, 100) + defer bc.Stop() + + // Create a request with more hashes than the cap + hashes := make([]common.Hash, maxAccessListLookups+100) + for i := range hashes { + hashes[i] = common.BytesToHash([]byte{byte(i), byte(i >> 8)}) + } + req := &GetAccessListsPacket{ + ID: 3, + Hashes: hashes, + } + result := ServiceGetAccessListsQuery(bc, req) + + // Can't get more than maxAccessListLookups results + if len(result) > maxAccessListLookups { + t.Fatalf("expected at most %d results, got %d", maxAccessListLookups, len(result)) + } +} + +// TestServiceGetAccessListsQueryByteLimit verifies that the response stops +// once the byte limit is exceeded. The handler appends the entry that crosses +// the limit before breaking, so the total size will exceed the limit by at +// most one BAL. +func TestServiceGetAccessListsQueryByteLimit(t *testing.T) { + t.Parallel() + + // The handler will return 3/5 entries (3MB total) then break. + balSize := 1024 * 1024 + nBlocks := 5 + bc, hashes, _ := getChainWithBALs(nBlocks, balSize) + defer bc.Stop() + req := &GetAccessListsPacket{ + ID: 0, + Hashes: hashes, + } + result := ServiceGetAccessListsQuery(bc, req) + + // Should have stopped before returning all blocks + if len(result) >= nBlocks { + t.Fatalf("expected fewer than %d results due to byte limit, got %d", nBlocks, len(result)) + } + + // Should have returned at least one + if len(result) == 0 { + t.Fatal("expected at least one result") + } + + // The total size should exceed the limit (the entry that crosses it is included) + var total uint64 + for _, bal := range result { + total += uint64(len(bal)) + } + if total <= softResponseLimit { + t.Errorf("total response size %d should exceed soft limit %d (includes one entry past limit)", total, softResponseLimit) + } +} diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go new file mode 100644 index 0000000000..64522343f9 --- /dev/null +++ b/eth/protocols/snap/handlers.go @@ -0,0 +1,593 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package snap + +import ( + "bytes" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/tracker" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/ethereum/go-ethereum/triedb/database" +) + +func handleGetAccountRange(backend Backend, msg Decoder, peer *Peer) error { + var req GetAccountRangePacket + if err := msg.Decode(&req); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + // Service the request, potentially returning nothing in case of errors + accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req) + + // Send back anything accumulated (or empty in case of errors) + return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{ + ID: req.ID, + Accounts: accounts, + Proof: proofs, + }) +} + +// ServiceGetAccountRangeQuery assembles the response to an account range query. +// It is exposed to allow external packages to test protocol behavior. +func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) { + if req.Bytes > softResponseLimit { + req.Bytes = softResponseLimit + } + // Retrieve the requested state and bail out if non existent + tr, err := trie.New(trie.StateTrieID(req.Root), chain.TrieDB()) + if err != nil { + return nil, nil + } + // Temporary solution: using the snapshot interface for both cases. + // This can be removed once the hash scheme is deprecated. + var it snapshot.AccountIterator + if chain.TrieDB().Scheme() == rawdb.HashScheme { + // The snapshot is assumed to be available in hash mode if + // the SNAP protocol is enabled. + it, err = chain.Snapshots().AccountIterator(req.Root, req.Origin) + } else { + it, err = chain.TrieDB().AccountIterator(req.Root, req.Origin) + } + if err != nil { + return nil, nil + } + // Iterate over the requested range and pile accounts up + var ( + accounts []*AccountData + size uint64 + last common.Hash + ) + for it.Next() { + hash, account := it.Hash(), common.CopyBytes(it.Account()) + + // Track the returned interval for the Merkle proofs + last = hash + + // Assemble the reply item + size += uint64(common.HashLength + len(account)) + accounts = append(accounts, &AccountData{ + Hash: hash, + Body: account, + }) + // If we've exceeded the request threshold, abort + if bytes.Compare(hash[:], req.Limit[:]) >= 0 { + break + } + if size > req.Bytes { + break + } + } + it.Release() + + // Generate the Merkle proofs for the first and last account + proof := trienode.NewProofSet() + if err := tr.Prove(req.Origin[:], proof); err != nil { + log.Warn("Failed to prove account range", "origin", req.Origin, "err", err) + return nil, nil + } + if last != (common.Hash{}) { + if err := tr.Prove(last[:], proof); err != nil { + log.Warn("Failed to prove account range", "last", last, "err", err) + return nil, nil + } + } + return accounts, proof.List() +} + +func handleAccountRange(backend Backend, msg Decoder, peer *Peer) error { + res := new(accountRangeInput) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + + // Check response validity. + if len := res.Proof.Len(); len > 128 { + return fmt.Errorf("AccountRange: invalid proof (length %d)", len) + } + tresp := tracker.Response{ID: res.ID, MsgCode: AccountRangeMsg, Size: len(res.Accounts.Content())} + if err := peer.tracker.Fulfil(tresp); err != nil { + return err + } + + // Decode. + accounts, err := res.Accounts.Items() + if err != nil { + return fmt.Errorf("AccountRange: invalid accounts list: %v", err) + } + proof, err := res.Proof.Items() + if err != nil { + return fmt.Errorf("AccountRange: invalid proof: %v", err) + } + + // Ensure the range is monotonically increasing + for i := 1; i < len(accounts); i++ { + if bytes.Compare(accounts[i-1].Hash[:], accounts[i].Hash[:]) >= 0 { + return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, accounts[i-1].Hash[:], i, accounts[i].Hash[:]) + } + } + + return backend.Handle(peer, &AccountRangePacket{res.ID, accounts, proof}) +} + +func handleGetStorageRanges(backend Backend, msg Decoder, peer *Peer) error { + var req GetStorageRangesPacket + if err := msg.Decode(&req); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + // Service the request, potentially returning nothing in case of errors + slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req) + + // Send back anything accumulated (or empty in case of errors) + return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{ + ID: req.ID, + Slots: slots, + Proof: proofs, + }) +} + +func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) { + if req.Bytes > softResponseLimit { + req.Bytes = softResponseLimit + } + // TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set? + // TODO(karalabe): - Logging locally is not ideal as remote faults annoy the local user + // TODO(karalabe): - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional) + + // Calculate the hard limit at which to abort, even if mid storage trie + hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack)) + + // Retrieve storage ranges until the packet limit is reached + var ( + slots [][]*StorageData + proofs [][]byte + size uint64 + ) + for _, account := range req.Accounts { + // If we've exceeded the requested data limit, abort without opening + // a new storage range (that we'd need to prove due to exceeded size) + if size >= req.Bytes { + break + } + // The first account might start from a different origin and end sooner + var origin common.Hash + if len(req.Origin) > 0 { + origin, req.Origin = common.BytesToHash(req.Origin), nil + } + var limit = common.MaxHash + if len(req.Limit) > 0 { + limit, req.Limit = common.BytesToHash(req.Limit), nil + } + // Retrieve the requested state and bail out if non existent + var ( + err error + it snapshot.StorageIterator + ) + // Temporary solution: using the snapshot interface for both cases. + // This can be removed once the hash scheme is deprecated. + if chain.TrieDB().Scheme() == rawdb.HashScheme { + // The snapshot is assumed to be available in hash mode if + // the SNAP protocol is enabled. + it, err = chain.Snapshots().StorageIterator(req.Root, account, origin) + } else { + it, err = chain.TrieDB().StorageIterator(req.Root, account, origin) + } + if err != nil { + return nil, nil + } + // Iterate over the requested range and pile slots up + var ( + storage []*StorageData + last common.Hash + abort bool + ) + for it.Next() { + if size >= hardLimit { + abort = true + break + } + hash, slot := it.Hash(), common.CopyBytes(it.Slot()) + + // Track the returned interval for the Merkle proofs + last = hash + + // Assemble the reply item + size += uint64(common.HashLength + len(slot)) + storage = append(storage, &StorageData{ + Hash: hash, + Body: slot, + }) + // If we've exceeded the request threshold, abort + if bytes.Compare(hash[:], limit[:]) >= 0 { + break + } + } + if len(storage) > 0 { + slots = append(slots, storage) + } + it.Release() + + // Generate the Merkle proofs for the first and last storage slot, but + // only if the response was capped. If the entire storage trie included + // in the response, no need for any proofs. + if origin != (common.Hash{}) || (abort && len(storage) > 0) { + // Request started at a non-zero hash or was capped prematurely, add + // the endpoint Merkle proofs + accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.TrieDB()) + if err != nil { + return nil, nil + } + acc, err := accTrie.GetAccountByHash(account) + if err != nil || acc == nil { + return nil, nil + } + id := trie.StorageTrieID(req.Root, account, acc.Root) + stTrie, err := trie.NewStateTrie(id, chain.TrieDB()) + if err != nil { + return nil, nil + } + proof := trienode.NewProofSet() + if err := stTrie.Prove(origin[:], proof); err != nil { + log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err) + return nil, nil + } + if last != (common.Hash{}) { + if err := stTrie.Prove(last[:], proof); err != nil { + log.Warn("Failed to prove storage range", "last", last, "err", err) + return nil, nil + } + } + proofs = append(proofs, proof.List()...) + // Proof terminates the reply as proofs are only added if a node + // refuses to serve more data (exception when a contract fetch is + // finishing, but that's that). + break + } + } + return slots, proofs +} + +func handleStorageRanges(backend Backend, msg Decoder, peer *Peer) error { + res := new(storageRangesInput) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + + // Check response validity. + if len := res.Proof.Len(); len > 128 { + return fmt.Errorf("StorageRangesMsg: invalid proof (length %d)", len) + } + tresp := tracker.Response{ID: res.ID, MsgCode: StorageRangesMsg, Size: len(res.Slots.Content())} + if err := peer.tracker.Fulfil(tresp); err != nil { + return fmt.Errorf("StorageRangesMsg: %w", err) + } + + // Decode. + slotLists, err := res.Slots.Items() + if err != nil { + return fmt.Errorf("AccountRange: invalid accounts list: %v", err) + } + proof, err := res.Proof.Items() + if err != nil { + return fmt.Errorf("AccountRange: invalid proof: %v", err) + } + + // Ensure the ranges are monotonically increasing + for i, slots := range slotLists { + for j := 1; j < len(slots); j++ { + if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 { + return fmt.Errorf("storage slots not monotonically increasing for account #%d: #%d [%x] vs #%d [%x]", i, j-1, slots[j-1].Hash[:], j, slots[j].Hash[:]) + } + } + } + + return backend.Handle(peer, &StorageRangesPacket{res.ID, slotLists, proof}) +} + +func handleGetByteCodes(backend Backend, msg Decoder, peer *Peer) error { + var req GetByteCodesPacket + if err := msg.Decode(&req); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + // Service the request, potentially returning nothing in case of errors + codes := ServiceGetByteCodesQuery(backend.Chain(), &req) + + // Send back anything accumulated (or empty in case of errors) + return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{ + ID: req.ID, + Codes: codes, + }) +} + +// ServiceGetByteCodesQuery assembles the response to a byte codes query. +// It is exposed to allow external packages to test protocol behavior. +func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte { + if req.Bytes > softResponseLimit { + req.Bytes = softResponseLimit + } + if len(req.Hashes) > maxCodeLookups { + req.Hashes = req.Hashes[:maxCodeLookups] + } + // Retrieve bytecodes until the packet size limit is reached + var ( + codes [][]byte + bytes uint64 + ) + for _, hash := range req.Hashes { + if hash == types.EmptyCodeHash { + // Peers should not request the empty code, but if they do, at + // least sent them back a correct response without db lookups + codes = append(codes, []byte{}) + } else if blob := chain.ContractCodeWithPrefix(hash); len(blob) > 0 { + codes = append(codes, blob) + bytes += uint64(len(blob)) + } + if bytes > req.Bytes { + break + } + } + return codes +} + +func handleByteCodes(backend Backend, msg Decoder, peer *Peer) error { + res := new(byteCodesInput) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + + length := res.Codes.Len() + tresp := tracker.Response{ID: res.ID, MsgCode: ByteCodesMsg, Size: length} + if err := peer.tracker.Fulfil(tresp); err != nil { + return fmt.Errorf("ByteCodes: %w", err) + } + + codes, err := res.Codes.Items() + if err != nil { + return fmt.Errorf("ByteCodes: %w", err) + } + + return backend.Handle(peer, &ByteCodesPacket{res.ID, codes}) +} + +func handleGetTrienodes(backend Backend, msg Decoder, peer *Peer) error { + var req GetTrieNodesPacket + if err := msg.Decode(&req); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + // Service the request, potentially returning nothing in case of errors + nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req) + if err != nil { + return err + } + // Send back anything accumulated (or empty in case of errors) + return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{ + ID: req.ID, + Nodes: nodes, + }) +} + +func nextBytes(it *rlp.Iterator) []byte { + if !it.Next() { + return nil + } + content, _, err := rlp.SplitString(it.Value()) + if err != nil { + return nil + } + return content +} + +// ServiceGetTrieNodesQuery assembles the response to a trie nodes query. +// It is exposed to allow external packages to test protocol behavior. +func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) ([][]byte, error) { + start := time.Now() + if req.Bytes > softResponseLimit { + req.Bytes = softResponseLimit + } + // Make sure we have the state associated with the request + triedb := chain.TrieDB() + + accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb) + if err != nil { + // We don't have the requested state available, bail out + return nil, nil + } + // The 'reader' might be nil, in which case we cannot serve storage slots + // via snapshot. + var reader database.StateReader + if chain.Snapshots() != nil { + reader = chain.Snapshots().Snapshot(req.Root) + } + if reader == nil { + reader, _ = triedb.StateReader(req.Root) + } + + // Retrieve trie nodes until the packet size limit is reached + var ( + outerIt = req.Paths.ContentIterator() + nodes [][]byte + bytes uint64 + loads int // Trie hash expansions to count database reads + ) + for outerIt.Next() { + innerIt, err := rlp.NewListIterator(outerIt.Value()) + if err != nil { + return nodes, err + } + + switch innerIt.Count() { + case 0: + // Ensure we penalize invalid requests + return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest) + + case 1: + // If we're only retrieving an account trie node, fetch it directly + accKey := nextBytes(&innerIt) + if accKey == nil { + return nodes, fmt.Errorf("%w: invalid account node request", errBadRequest) + } + blob, resolved, err := accTrie.GetNode(accKey) + loads += resolved // always account database reads, even for failures + if err != nil { + break + } + nodes = append(nodes, blob) + bytes += uint64(len(blob)) + + default: + // Storage slots requested, open the storage trie and retrieve from there + accKey := nextBytes(&innerIt) + if accKey == nil { + return nodes, fmt.Errorf("%w: invalid account storage request", errBadRequest) + } + var stRoot common.Hash + if reader == nil { + // We don't have the requested state snapshotted yet (or it is stale), + // but can look up the account via the trie instead. + account, err := accTrie.GetAccountByHash(common.BytesToHash(accKey)) + loads += 8 // We don't know the exact cost of lookup, this is an estimate + if err != nil || account == nil { + break + } + stRoot = account.Root + } else { + account, err := reader.Account(common.BytesToHash(accKey)) + loads++ // always account database reads, even for failures + if err != nil || account == nil { + break + } + stRoot = common.BytesToHash(account.Root) + } + + id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot) + stTrie, err := trie.NewStateTrie(id, triedb) + loads++ // always account database reads, even for failures + if err != nil { + break + } + for innerIt.Next() { + path, _, err := rlp.SplitString(innerIt.Value()) + if err != nil { + return nil, fmt.Errorf("%w: invalid storage key: %v", errBadRequest, err) + } + blob, resolved, err := stTrie.GetNode(path) + loads += resolved // always account database reads, even for failures + if err != nil { + break + } + nodes = append(nodes, blob) + bytes += uint64(len(blob)) + + // Sanity check limits to avoid DoS on the store trie loads + if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { + break + } + } + } + // Abort request processing if we've exceeded our limits + if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { + break + } + } + return nodes, nil +} + +func handleTrieNodes(backend Backend, msg Decoder, peer *Peer) error { + res := new(trieNodesInput) + if err := msg.Decode(res); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + + tresp := tracker.Response{ID: res.ID, MsgCode: TrieNodesMsg, Size: res.Nodes.Len()} + if err := peer.tracker.Fulfil(tresp); err != nil { + return fmt.Errorf("TrieNodes: %w", err) + } + nodes, err := res.Nodes.Items() + if err != nil { + return fmt.Errorf("TrieNodes: %w", err) + } + + return backend.Handle(peer, &TrieNodesPacket{res.ID, nodes}) +} + +// nolint:unused +func handleGetAccessLists(backend Backend, msg Decoder, peer *Peer) error { + var req GetAccessListsPacket + if err := msg.Decode(&req); err != nil { + return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) + } + bals := ServiceGetAccessListsQuery(backend.Chain(), &req) + return p2p.Send(peer.rw, AccessListsMsg, &AccessListsPacket{ + ID: req.ID, + AccessLists: bals, + }) +} + +// ServiceGetAccessListsQuery assembles the response to an access list query. +// It is exposed to allow external packages to test protocol behavior. +func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacket) []rlp.RawValue { + // Cap the number of lookups + if len(req.Hashes) > maxAccessListLookups { + req.Hashes = req.Hashes[:maxAccessListLookups] + } + var ( + bals []rlp.RawValue + bytes uint64 + ) + for _, hash := range req.Hashes { + if bal := chain.GetAccessListRLP(hash); len(bal) > 0 { + bals = append(bals, bal) + bytes += uint64(len(bal)) + } else { + // Either the block is unknown or the BAL doesn't exist + bals = append(bals, nil) + } + if bytes > softResponseLimit { + break + } + } + return bals +} diff --git a/eth/protocols/snap/protocol.go b/eth/protocols/snap/protocol.go index 25fe25822b..57b29bbe36 100644 --- a/eth/protocols/snap/protocol.go +++ b/eth/protocols/snap/protocol.go @@ -28,6 +28,7 @@ import ( // Constants to match up protocol versions and messages const ( SNAP1 = 1 + //SNAP2 = 2 ) // ProtocolName is the official short name of the `snap` protocol used during @@ -40,7 +41,7 @@ var ProtocolVersions = []uint{SNAP1} // protocolLengths are the number of implemented message corresponding to // different protocol versions. -var protocolLengths = map[uint]uint64{SNAP1: 8} +var protocolLengths = map[uint]uint64{ /*SNAP2: 10,*/ SNAP1: 8} // maxMessageSize is the maximum cap on the size of a protocol message. const maxMessageSize = 10 * 1024 * 1024 @@ -54,6 +55,8 @@ const ( ByteCodesMsg = 0x05 GetTrieNodesMsg = 0x06 TrieNodesMsg = 0x07 + GetAccessListsMsg = 0x08 + AccessListsMsg = 0x09 ) var ( @@ -215,6 +218,20 @@ type TrieNodesPacket struct { Nodes [][]byte // Requested state trie nodes } +// GetAccessListsPacket requests BALs for a set of block hashes. +type GetAccessListsPacket struct { + ID uint64 // Request ID to match up responses with + Hashes []common.Hash // Block hashes to retrieve BALs for +} + +// AccessListsPacket is the response to GetAccessListsPacket. +// Each entry corresponds to the requested hash at the same index. +// Empty entries indicate the BAL is unavailable. +type AccessListsPacket struct { + ID uint64 // ID of the request this is a response for + AccessLists []rlp.RawValue // Requested BALs +} + func (*GetAccountRangePacket) Name() string { return "GetAccountRange" } func (*GetAccountRangePacket) Kind() byte { return GetAccountRangeMsg } @@ -238,3 +255,9 @@ func (*GetTrieNodesPacket) Kind() byte { return GetTrieNodesMsg } func (*TrieNodesPacket) Name() string { return "TrieNodes" } func (*TrieNodesPacket) Kind() byte { return TrieNodesMsg } + +func (*GetAccessListsPacket) Name() string { return "GetAccessLists" } +func (*GetAccessListsPacket) Kind() byte { return GetAccessListsMsg } + +func (*AccessListsPacket) Name() string { return "AccessLists" } +func (*AccessListsPacket) Kind() byte { return AccessListsMsg } From a608ac94ecdae126f90372cbfb5ff88528b35ffa Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:53:54 -0500 Subject: [PATCH 10/27] eth/protocols/snap: restore Bytes soft limit to GetAccessListsPacket (#34649) This PR adds Bytes field back to GetAccesListsPacket --- eth/protocols/snap/handler_test.go | 4 ++++ eth/protocols/snap/handlers.go | 5 ++++- eth/protocols/snap/protocol.go | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go index cb4b378a8d..53b22ca7f7 100644 --- a/eth/protocols/snap/handler_test.go +++ b/eth/protocols/snap/handler_test.go @@ -80,6 +80,7 @@ func TestServiceGetAccessListsQuery(t *testing.T) { req := &GetAccessListsPacket{ ID: 1, Hashes: hashes, + Bytes: softResponseLimit, } result := ServiceGetAccessListsQuery(bc, req) @@ -105,6 +106,7 @@ func TestServiceGetAccessListsQueryEmpty(t *testing.T) { req := &GetAccessListsPacket{ ID: 2, Hashes: mixed, + Bytes: softResponseLimit, } result := ServiceGetAccessListsQuery(bc, req) @@ -147,6 +149,7 @@ func TestServiceGetAccessListsQueryCap(t *testing.T) { req := &GetAccessListsPacket{ ID: 3, Hashes: hashes, + Bytes: softResponseLimit, } result := ServiceGetAccessListsQuery(bc, req) @@ -171,6 +174,7 @@ func TestServiceGetAccessListsQueryByteLimit(t *testing.T) { req := &GetAccessListsPacket{ ID: 0, Hashes: hashes, + Bytes: softResponseLimit, } result := ServiceGetAccessListsQuery(bc, req) diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go index 64522343f9..4d60aab1f6 100644 --- a/eth/protocols/snap/handlers.go +++ b/eth/protocols/snap/handlers.go @@ -569,6 +569,9 @@ func handleGetAccessLists(backend Backend, msg Decoder, peer *Peer) error { // ServiceGetAccessListsQuery assembles the response to an access list query. // It is exposed to allow external packages to test protocol behavior. func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacket) []rlp.RawValue { + if req.Bytes > softResponseLimit { + req.Bytes = softResponseLimit + } // Cap the number of lookups if len(req.Hashes) > maxAccessListLookups { req.Hashes = req.Hashes[:maxAccessListLookups] @@ -585,7 +588,7 @@ func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacke // Either the block is unknown or the BAL doesn't exist bals = append(bals, nil) } - if bytes > softResponseLimit { + if bytes > req.Bytes { break } } diff --git a/eth/protocols/snap/protocol.go b/eth/protocols/snap/protocol.go index 57b29bbe36..7913f8b053 100644 --- a/eth/protocols/snap/protocol.go +++ b/eth/protocols/snap/protocol.go @@ -222,6 +222,7 @@ type TrieNodesPacket struct { type GetAccessListsPacket struct { ID uint64 // Request ID to match up responses with Hashes []common.Hash // Block hashes to retrieve BALs for + Bytes uint64 // Soft limit at which to stop returning data } // AccessListsPacket is the response to GetAccessListsPacket. From d8cb8a962b2de18cac5f2b6a820a3dea5d33db0e Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Sat, 4 Apr 2026 21:00:07 +0800 Subject: [PATCH 11/27] core, eth, ethclient, triedb: report trienode index progress (#34633) The trienode history indexing progress is also exposed via an RPC endpoint and contributes to the eth_syncing status. --- core/blockchain_reader.go | 2 +- eth/api_backend.go | 5 +++-- eth/downloader/api.go | 5 +++-- ethclient/ethclient.go | 2 ++ graphql/graphql.go | 3 +++ interfaces.go | 7 ++++--- internal/ethapi/api.go | 1 + triedb/database.go | 4 ++-- triedb/pathdb/database.go | 23 +++++++++++++++++++---- triedb/pathdb/database_test.go | 4 ++-- 10 files changed, 40 insertions(+), 16 deletions(-) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 8b026680d2..3614702d1a 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -476,7 +476,7 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) { } // StateIndexProgress returns the historical state indexing progress. -func (bc *BlockChain) StateIndexProgress() (uint64, error) { +func (bc *BlockChain) StateIndexProgress() (uint64, uint64, error) { return bc.triedb.IndexProgress() } diff --git a/eth/api_backend.go b/eth/api_backend.go index fe2105f47b..a4e976b1b8 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -414,9 +414,10 @@ func (b *EthAPIBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress prog.TxIndexFinishedBlocks = txProg.Indexed prog.TxIndexRemainingBlocks = txProg.Remaining } - remain, err := b.eth.blockchain.StateIndexProgress() + stateRemain, trienodeRemain, err := b.eth.blockchain.StateIndexProgress() if err == nil { - prog.StateIndexRemaining = remain + prog.StateIndexRemaining = stateRemain + prog.TrienodeIndexRemaining = trienodeRemain } return prog } diff --git a/eth/downloader/api.go b/eth/downloader/api.go index f97371de5f..1fea35775e 100644 --- a/eth/downloader/api.go +++ b/eth/downloader/api.go @@ -81,9 +81,10 @@ func (api *DownloaderAPI) eventLoop() { prog.TxIndexFinishedBlocks = txProg.Indexed prog.TxIndexRemainingBlocks = txProg.Remaining } - remain, err := api.chain.StateIndexProgress() + stateRemain, trienodeRemain, err := api.chain.StateIndexProgress() if err == nil { - prog.StateIndexRemaining = remain + prog.StateIndexRemaining = stateRemain + prog.TrienodeIndexRemaining = trienodeRemain } return prog } diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index bc4eaad6fa..85286ec919 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -838,6 +838,7 @@ type rpcProgress struct { TxIndexFinishedBlocks hexutil.Uint64 TxIndexRemainingBlocks hexutil.Uint64 StateIndexRemaining hexutil.Uint64 + TrienodeIndexRemaining hexutil.Uint64 } func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress { @@ -865,6 +866,7 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress { TxIndexFinishedBlocks: uint64(p.TxIndexFinishedBlocks), TxIndexRemainingBlocks: uint64(p.TxIndexRemainingBlocks), StateIndexRemaining: uint64(p.StateIndexRemaining), + TrienodeIndexRemaining: uint64(p.TrienodeIndexRemaining), } } diff --git a/graphql/graphql.go b/graphql/graphql.go index f25bfd127a..dadc91fac0 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -1531,6 +1531,9 @@ func (s *SyncState) TxIndexRemainingBlocks() hexutil.Uint64 { func (s *SyncState) StateIndexRemaining() hexutil.Uint64 { return hexutil.Uint64(s.progress.StateIndexRemaining) } +func (s *SyncState) TrienodeIndexRemaining() hexutil.Uint64 { + return hexutil.Uint64(s.progress.TrienodeIndexRemaining) +} // Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not // yet received the latest block headers from its peers. In case it is synchronizing: diff --git a/interfaces.go b/interfaces.go index 21d42c6d34..8b3dbe3a42 100644 --- a/interfaces.go +++ b/interfaces.go @@ -139,8 +139,9 @@ type SyncProgress struct { TxIndexFinishedBlocks uint64 // Number of blocks whose transactions are already indexed TxIndexRemainingBlocks uint64 // Number of blocks whose transactions are not indexed yet - // "historical state indexing" fields - StateIndexRemaining uint64 // Number of states remain unindexed + // "historical data indexing" fields + StateIndexRemaining uint64 // Number of states remain unindexed + TrienodeIndexRemaining uint64 // Number of trienodes remain unindexed } // Done returns the indicator if the initial sync is finished or not. @@ -148,7 +149,7 @@ func (prog SyncProgress) Done() bool { if prog.CurrentBlock < prog.HighestBlock { return false } - return prog.TxIndexRemainingBlocks == 0 && prog.StateIndexRemaining == 0 + return prog.TxIndexRemainingBlocks == 0 && prog.StateIndexRemaining == 0 && prog.TrienodeIndexRemaining == 0 } // ChainSyncReader wraps access to the node's current sync status. If there's no diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 694fbf9c15..149e12c5b8 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -180,6 +180,7 @@ func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) { "txIndexFinishedBlocks": hexutil.Uint64(progress.TxIndexFinishedBlocks), "txIndexRemainingBlocks": hexutil.Uint64(progress.TxIndexRemainingBlocks), "stateIndexRemaining": hexutil.Uint64(progress.StateIndexRemaining), + "trienodeIndexRemaining": hexutil.Uint64(progress.TrienodeIndexRemaining), }, nil } diff --git a/triedb/database.go b/triedb/database.go index e7e47bb91a..c1abe93462 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -367,10 +367,10 @@ func (db *Database) StorageIterator(root common.Hash, account common.Hash, seek // IndexProgress returns the indexing progress made so far. It provides the // number of states that remain unindexed. -func (db *Database) IndexProgress() (uint64, error) { +func (db *Database) IndexProgress() (uint64, uint64, error) { pdb, ok := db.backend.(*pathdb.Database) if !ok { - return 0, errors.New("not supported") + return 0, 0, errors.New("not supported") } return pdb.IndexProgress() } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 86a42c69f4..a61d302b1d 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -626,11 +626,26 @@ func (db *Database) HistoryRange() (uint64, uint64, error) { // IndexProgress returns the indexing progress made so far. It provides the // number of states that remain unindexed. -func (db *Database) IndexProgress() (uint64, error) { - if db.stateIndexer == nil { - return 0, nil +func (db *Database) IndexProgress() (uint64, uint64, error) { + var ( + stateProgress uint64 + trieProgress uint64 + ) + if db.stateIndexer != nil { + prog, err := db.stateIndexer.progress() + if err != nil { + return 0, 0, err + } + stateProgress = prog } - return db.stateIndexer.progress() + if db.trienodeIndexer != nil { + prog, err := db.trienodeIndexer.progress() + if err != nil { + return 0, 0, err + } + trieProgress = prog + } + return stateProgress, trieProgress, nil } // AccountIterator creates a new account iterator for the specified root hash and diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index 8ece83cad7..e70a3ec2a2 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -987,7 +987,7 @@ func TestDatabaseIndexRecovery(t *testing.T) { t.Fatalf("Unexpected state history found, %d", i) } } - remain, err := env.db.IndexProgress() + remain, _, err := env.db.IndexProgress() if err != nil { t.Fatalf("Failed to obtain the progress, %v", err) } @@ -1001,7 +1001,7 @@ func TestDatabaseIndexRecovery(t *testing.T) { panic(fmt.Errorf("failed to update state changes, err: %w", err)) } } - remain, err = env.db.IndexProgress() + remain, _, err = env.db.IndexProgress() if err != nil { t.Fatalf("Failed to obtain the progress, %v", err) } From 44257950f132cf0d7bfd7a96d4447d12567412a2 Mon Sep 17 00:00:00 2001 From: Martin HS Date: Tue, 7 Apr 2026 08:13:25 +0200 Subject: [PATCH 12/27] tests: enable execution of amsterdam statetests (#34671) :wave: This PR makes it possible to run "Amsterdam" in statetests. I'm aware that they'll be failing and not in consensus with other clients, yet, but it's nice to be able to run tests and see what works and what doesn't Before the change: ``` $ go run ./cmd/evm statetest ./amsterdam.json [ { "name": "00000019-mixed-1", "pass": false, "fork": "Amsterdam", "error": "unexpected error: unsupported fork \"Amsterdam\"" } ] ``` After ``` $ go run ./cmd/evm statetest ./amsterdam.json {"stateRoot": "0x25b78260b76493a783c77c513125c8b0c5d24e058b4e87130bbe06f1d8b9419e"} [ { "name": "00000019-mixed-1", "pass": false, "stateRoot": "0x25b78260b76493a783c77c513125c8b0c5d24e058b4e87130bbe06f1d8b9419e", "fork": "Amsterdam", "error": "post state root mismatch: got 25b78260b76493a783c77c513125c8b0c5d24e058b4e87130bbe06f1d8b9419e, want 0000000000000000000000000000000000000000000000000000000000000000" } ] ``` --- tests/init.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/init.go b/tests/init.go index d10b47986c..f115e427a5 100644 --- a/tests/init.go +++ b/tests/init.go @@ -720,6 +720,43 @@ var Forks = map[string]*params.ChainConfig{ BPO4: params.DefaultBPO4BlobConfig, }, }, + "Amsterdam": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(0), + PragueTime: u64(0), + OsakaTime: u64(0), + BPO1Time: u64(0), + BPO2Time: u64(0), + BPO3Time: u64(0), + BPO4Time: u64(0), + AmsterdamTime: u64(0), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + BPO1: bpo1BlobConfig, + BPO2: bpo2BlobConfig, + BPO3: params.DefaultBPO3BlobConfig, + BPO4: params.DefaultBPO4BlobConfig, + Amsterdam: params.DefaultBPO4BlobConfig, // TODO update when defined + }, + }, "Verkle": { ChainID: big.NewInt(1), HomesteadBlock: big.NewInt(0), From bd6530a1d479893a881d49e1157fd0a4425e9399 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:36:53 -0500 Subject: [PATCH 13/27] triedb, triedb/internal, triedb/pathdb: add GenerateTrie + extract shared pipeline into triedb/internal (#34654) This PR adds `GenerateTrie(db, scheme, root)` to the `triedb` package, which rebuilds all tries from flat snapshot KV data. This is needed by snap/2 sync so it can rebuild the trie after downloading the flat state. The shared trie generation pipeline from `pathdb/verifier.go` was moved into `triedb/internal/conversion.go` so both `GenerateTrie` and `VerifyState` reuse the same code. --- triedb/generate.go | 108 ++++++++++ triedb/generate_test.go | 178 +++++++++++++++++ triedb/internal/conversion.go | 363 ++++++++++++++++++++++++++++++++++ triedb/pathdb/iterator.go | 47 +---- triedb/pathdb/verifier.go | 289 +-------------------------- 5 files changed, 663 insertions(+), 322 deletions(-) create mode 100644 triedb/generate.go create mode 100644 triedb/generate_test.go create mode 100644 triedb/internal/conversion.go diff --git a/triedb/generate.go b/triedb/generate.go new file mode 100644 index 0000000000..259e139848 --- /dev/null +++ b/triedb/generate.go @@ -0,0 +1,108 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package triedb + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/triedb/internal" +) + +// kvAccountIterator wraps an ethdb.Iterator to iterate over account snapshot +// entries in the database, implementing internal.AccountIterator. +type kvAccountIterator struct { + it ethdb.Iterator + hash common.Hash +} + +func newKVAccountIterator(db ethdb.Iteratee) *kvAccountIterator { + it := rawdb.NewKeyLengthIterator( + db.NewIterator(rawdb.SnapshotAccountPrefix, nil), + len(rawdb.SnapshotAccountPrefix)+common.HashLength, + ) + return &kvAccountIterator{it: it} +} + +func (it *kvAccountIterator) Next() bool { + if !it.it.Next() { + return false + } + key := it.it.Key() + copy(it.hash[:], key[len(rawdb.SnapshotAccountPrefix):]) + return true +} + +func (it *kvAccountIterator) Hash() common.Hash { return it.hash } +func (it *kvAccountIterator) Account() []byte { return it.it.Value() } +func (it *kvAccountIterator) Error() error { return it.it.Error() } +func (it *kvAccountIterator) Release() { it.it.Release() } + +// kvStorageIterator wraps an ethdb.Iterator to iterate over storage snapshot +// entries for a specific account, implementing internal.StorageIterator. +type kvStorageIterator struct { + it ethdb.Iterator + hash common.Hash +} + +func newKVStorageIterator(db ethdb.Iteratee, accountHash common.Hash) *kvStorageIterator { + it := rawdb.IterateStorageSnapshots(db, accountHash) + return &kvStorageIterator{it: it} +} + +func (it *kvStorageIterator) Next() bool { + if !it.it.Next() { + return false + } + key := it.it.Key() + copy(it.hash[:], key[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]) + return true +} + +func (it *kvStorageIterator) Hash() common.Hash { return it.hash } +func (it *kvStorageIterator) Slot() []byte { return it.it.Value() } +func (it *kvStorageIterator) Error() error { return it.it.Error() } +func (it *kvStorageIterator) Release() { it.it.Release() } + +// GenerateTrie rebuilds all tries (storage + account) from flat snapshot data +// in the database. It reads account and storage snapshots from the KV store, +// builds tries using StackTrie with streaming node writes, and verifies the +// computed state root matches the expected root. +func GenerateTrie(db ethdb.Database, scheme string, root common.Hash) error { + acctIt := newKVAccountIterator(db) + defer acctIt.Release() + + got, err := internal.GenerateTrieRoot(db, scheme, acctIt, common.Hash{}, internal.StackTrieGenerate, func(dst ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) { + storageIt := newKVStorageIterator(db, accountHash) + defer storageIt.Release() + + hash, err := internal.GenerateTrieRoot(dst, scheme, storageIt, accountHash, internal.StackTrieGenerate, nil, stat, false) + if err != nil { + return common.Hash{}, err + } + return hash, nil + }, internal.NewGenerateStats(), true) + if err != nil { + return err + } + if got != root { + return fmt.Errorf("state root mismatch: got %x, want %x", got, root) + } + return nil +} diff --git a/triedb/generate_test.go b/triedb/generate_test.go new file mode 100644 index 0000000000..42bccd9aa3 --- /dev/null +++ b/triedb/generate_test.go @@ -0,0 +1,178 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package triedb + +import ( + "bytes" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" +) + +// testAccount is a helper for building test state with deterministic ordering. +type testAccount struct { + hash common.Hash + account types.StateAccount + storage []testSlot // must be sorted by hash +} + +type testSlot struct { + hash common.Hash + value []byte +} + +// buildExpectedRoot computes the state root from sorted test accounts using +// StackTrie (which requires sorted key insertion). +func buildExpectedRoot(t *testing.T, accounts []testAccount) common.Hash { + t.Helper() + // Sort accounts by hash + sort.Slice(accounts, func(i, j int) bool { + return bytes.Compare(accounts[i].hash[:], accounts[j].hash[:]) < 0 + }) + acctTrie := trie.NewStackTrie(nil) + for i := range accounts { + data, err := rlp.EncodeToBytes(&accounts[i].account) + if err != nil { + t.Fatal(err) + } + acctTrie.Update(accounts[i].hash[:], data) + } + return acctTrie.Hash() +} + +// computeStorageRoot computes the storage trie root from sorted slots. +func computeStorageRoot(slots []testSlot) common.Hash { + sort.Slice(slots, func(i, j int) bool { + return bytes.Compare(slots[i].hash[:], slots[j].hash[:]) < 0 + }) + st := trie.NewStackTrie(nil) + for _, s := range slots { + st.Update(s.hash[:], s.value) + } + return st.Hash() +} + +func TestGenerateTrieEmpty(t *testing.T) { + db := rawdb.NewMemoryDatabase() + if err := GenerateTrie(db, rawdb.HashScheme, types.EmptyRootHash); err != nil { + t.Fatalf("GenerateTrie on empty state failed: %v", err) + } +} + +func TestGenerateTrieAccountsOnly(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + accounts := []testAccount{ + { + hash: common.HexToHash("0x01"), + account: types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + }, + { + hash: common.HexToHash("0x02"), + account: types.StateAccount{ + Nonce: 2, + Balance: uint256.NewInt(200), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + }, + } + for _, a := range accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + } + root := buildExpectedRoot(t, accounts) + + if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil { + t.Fatalf("GenerateTrie failed: %v", err) + } +} + +func TestGenerateTrieWithStorage(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + slots := []testSlot{ + {hash: common.HexToHash("0xaa"), value: []byte{0x01, 0x02, 0x03}}, + {hash: common.HexToHash("0xbb"), value: []byte{0x04, 0x05, 0x06}}, + } + storageRoot := computeStorageRoot(slots) + + accounts := []testAccount{ + { + hash: common.HexToHash("0x01"), + account: types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + Root: storageRoot, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + storage: slots, + }, + { + hash: common.HexToHash("0x02"), + account: types.StateAccount{ + Nonce: 0, + Balance: uint256.NewInt(50), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + }, + } + // Write account snapshots + for _, a := range accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + } + // Write storage snapshots + for _, a := range accounts { + for _, s := range a.storage { + rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value) + } + } + root := buildExpectedRoot(t, accounts) + + if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil { + t.Fatalf("GenerateTrie failed: %v", err) + } +} + +func TestGenerateTrieRootMismatch(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + acct := types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + } + rawdb.WriteAccountSnapshot(db, common.HexToHash("0x01"), types.SlimAccountRLP(acct)) + + wrongRoot := common.HexToHash("0xdeadbeef") + err := GenerateTrie(db, rawdb.HashScheme, wrongRoot) + if err == nil { + t.Fatal("expected error for root mismatch, got nil") + } +} diff --git a/triedb/internal/conversion.go b/triedb/internal/conversion.go new file mode 100644 index 0000000000..b331b63e21 --- /dev/null +++ b/triedb/internal/conversion.go @@ -0,0 +1,363 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package internal contains shared trie generation utilities used by both +// triedb and triedb/pathdb. All code is ported from +// core/state/snapshot/conversion.go (with exported names) unless noted. +package internal + +import ( + "encoding/binary" + "fmt" + "math" + "runtime" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +// Iterator is an iterator to step over all the accounts or the specific +// storage in a snapshot which may or may not be composed of multiple layers. +type Iterator interface { + // Next steps the iterator forward one element, returning false if exhausted, + // or an error if iteration failed for some reason (e.g. root being iterated + // becomes stale and garbage collected). + Next() bool + + // Error returns any failure that occurred during iteration, which might have + // caused a premature iteration exit (e.g. snapshot stack becoming stale). + Error() error + + // Hash returns the hash of the account or storage slot the iterator is + // currently at. + Hash() common.Hash + + // Release releases associated resources. Release should always succeed and + // can be called multiple times without causing error. + Release() +} + +// AccountIterator is an iterator to step over all the accounts in a snapshot, +// which may or may not be composed of multiple layers. +type AccountIterator interface { + Iterator + + // Account returns the RLP encoded slim account the iterator is currently at. + // An error will be returned if the iterator becomes invalid + Account() []byte +} + +// StorageIterator is an iterator to step over the specific storage in a snapshot, +// which may or may not be composed of multiple layers. +type StorageIterator interface { + Iterator + + // Slot returns the storage slot the iterator is currently at. An error will + // be returned if the iterator becomes invalid + Slot() []byte +} + +// TrieKV represents a trie key-value pair. +type TrieKV struct { + Key common.Hash + Value []byte +} + +type ( + // TrieGeneratorFn is the interface of trie generation which can + // be implemented by different trie algorithm. + TrieGeneratorFn func(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan (TrieKV), out chan (common.Hash)) + + // LeafCallbackFn is the callback invoked at the leaves of the trie, + // returns the subtrie root with the specified subtrie identifier. + LeafCallbackFn func(db ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *GenerateStats) (common.Hash, error) +) + +// GenerateStats is a collection of statistics gathered by the trie generator +// for logging purposes. +type GenerateStats struct { + head common.Hash + start time.Time + + accounts uint64 // Number of accounts done (including those being crawled) + slots uint64 // Number of storage slots done (including those being crawled) + + slotsStart map[common.Hash]time.Time // Start time for account slot crawling + slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled + + lock sync.RWMutex +} + +// NewGenerateStats creates a new generator stats. +func NewGenerateStats() *GenerateStats { + return &GenerateStats{ + slotsStart: make(map[common.Hash]time.Time), + slotsHead: make(map[common.Hash]common.Hash), + start: time.Now(), + } +} + +// ProgressAccounts updates the generator stats for the account range. +func (stat *GenerateStats) ProgressAccounts(account common.Hash, done uint64) { + stat.lock.Lock() + defer stat.lock.Unlock() + + stat.accounts += done + stat.head = account +} + +// FinishAccounts updates the generator stats for the finished account range. +func (stat *GenerateStats) FinishAccounts(done uint64) { + stat.lock.Lock() + defer stat.lock.Unlock() + + stat.accounts += done +} + +// ProgressContract updates the generator stats for a specific in-progress contract. +func (stat *GenerateStats) ProgressContract(account common.Hash, slot common.Hash, done uint64) { + stat.lock.Lock() + defer stat.lock.Unlock() + + stat.slots += done + stat.slotsHead[account] = slot + if _, ok := stat.slotsStart[account]; !ok { + stat.slotsStart[account] = time.Now() + } +} + +// FinishContract updates the generator stats for a specific just-finished contract. +func (stat *GenerateStats) FinishContract(account common.Hash, done uint64) { + stat.lock.Lock() + defer stat.lock.Unlock() + + stat.slots += done + delete(stat.slotsHead, account) + delete(stat.slotsStart, account) +} + +// Report prints the cumulative progress statistic smartly. +func (stat *GenerateStats) Report() { + stat.lock.RLock() + defer stat.lock.RUnlock() + + ctx := []interface{}{ + "accounts", stat.accounts, + "slots", stat.slots, + "elapsed", common.PrettyDuration(time.Since(stat.start)), + } + if stat.accounts > 0 { + if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 { + var ( + left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts + eta = common.CalculateETA(done, left, time.Since(stat.start)) + ) + // If there are large contract crawls in progress, estimate their finish time + for acc, head := range stat.slotsHead { + start := stat.slotsStart[acc] + if done := binary.BigEndian.Uint64(head[:8]); done > 0 { + left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8]) + + // Override the ETA if larger than the largest until now + if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA { + eta = slotETA + } + } + } + ctx = append(ctx, []interface{}{ + "eta", common.PrettyDuration(eta), + }...) + } + } + log.Info("Iterating state snapshot", ctx...) +} + +// ReportDone prints the last log when the whole generation is finished. +func (stat *GenerateStats) ReportDone() { + stat.lock.RLock() + defer stat.lock.RUnlock() + + var ctx []interface{} + ctx = append(ctx, []interface{}{"accounts", stat.accounts}...) + if stat.slots != 0 { + ctx = append(ctx, []interface{}{"slots", stat.slots}...) + } + ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...) + log.Info("Iterated snapshot", ctx...) +} + +// RunReport periodically prints the progress information. +func RunReport(stats *GenerateStats, stop chan bool) { + timer := time.NewTimer(0) + defer timer.Stop() + + for { + select { + case <-timer.C: + stats.Report() + timer.Reset(time.Second * 8) + case success := <-stop: + if success { + stats.ReportDone() + } + return + } + } +} + +// GenerateTrieRoot generates the trie hash based on the snapshot iterator. +// It can be used for generating account trie, storage trie or even the +// whole state which connects the accounts and the corresponding storages. +func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn TrieGeneratorFn, leafCallback LeafCallbackFn, stats *GenerateStats, report bool) (common.Hash, error) { + var ( + in = make(chan TrieKV) // chan to pass leaves + out = make(chan common.Hash, 1) // chan to collect result + stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled + wg sync.WaitGroup + ) + // Spin up a go-routine for trie hash re-generation + wg.Add(1) + go func() { + defer wg.Done() + generatorFn(db, scheme, account, in, out) + }() + // Spin up a go-routine for progress logging + if report && stats != nil { + wg.Add(1) + go func() { + defer wg.Done() + RunReport(stats, stoplog) + }() + } + // Create a semaphore to assign tasks and collect results through. We'll pre- + // fill it with nils, thus using the same channel for both limiting concurrent + // processing and gathering results. + threads := runtime.NumCPU() + results := make(chan error, threads) + for i := 0; i < threads; i++ { + results <- nil // fill the semaphore + } + // stop is a helper function to shutdown the background threads + // and return the re-generated trie hash. + stop := func(fail error) (common.Hash, error) { + close(in) + result := <-out + for i := 0; i < threads; i++ { + if err := <-results; err != nil && fail == nil { + fail = err + } + } + stoplog <- fail == nil + + wg.Wait() + return result, fail + } + var ( + logged = time.Now() + processed = uint64(0) + leaf TrieKV + ) + // Start to feed leaves + for it.Next() { + if account == (common.Hash{}) { + var ( + err error + fullData []byte + ) + if leafCallback == nil { + fullData, err = types.FullAccountRLP(it.(AccountIterator).Account()) + if err != nil { + return stop(err) + } + } else { + // Wait until the semaphore allows us to continue, aborting if + // a sub-task failed + if err := <-results; err != nil { + results <- nil // stop will drain the results, add a noop back for this error we just consumed + return stop(err) + } + // Fetch the next account and process it concurrently + account, err := types.FullAccount(it.(AccountIterator).Account()) + if err != nil { + return stop(err) + } + go func(hash common.Hash) { + subroot, err := leafCallback(db, hash, common.BytesToHash(account.CodeHash), stats) + if err != nil { + results <- err + return + } + if account.Root != subroot { + results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot) + return + } + results <- nil + }(it.Hash()) + fullData, err = rlp.EncodeToBytes(account) + if err != nil { + return stop(err) + } + } + leaf = TrieKV{it.Hash(), fullData} + } else { + leaf = TrieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())} + } + in <- leaf + + // Accumulate the generation statistic if it's required. + processed++ + if time.Since(logged) > 3*time.Second && stats != nil { + if account == (common.Hash{}) { + stats.ProgressAccounts(it.Hash(), processed) + } else { + stats.ProgressContract(account, it.Hash(), processed) + } + logged, processed = time.Now(), 0 + } + } + // Commit the last part statistic. + if processed > 0 && stats != nil { + if account == (common.Hash{}) { + stats.FinishAccounts(processed) + } else { + stats.FinishContract(account, processed) + } + } + return stop(nil) +} + +// StackTrieGenerate is the trie generation function that creates a StackTrie +// and persists nodes via rawdb.WriteTrieNode. +func StackTrieGenerate(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan TrieKV, out chan common.Hash) { + var onTrieNode trie.OnTrieNode + if db != nil { + onTrieNode = func(path []byte, hash common.Hash, blob []byte) { + rawdb.WriteTrieNode(db, owner, path, hash, blob, scheme) + } + } + t := trie.NewStackTrie(onTrieNode) + for leaf := range in { + t.Update(leaf.Key[:], leaf.Value) + } + out <- t.Hash() +} diff --git a/triedb/pathdb/iterator.go b/triedb/pathdb/iterator.go index 8ca8247206..2d333dfa1b 100644 --- a/triedb/pathdb/iterator.go +++ b/triedb/pathdb/iterator.go @@ -24,48 +24,15 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/triedb/internal" ) -// Iterator is an iterator to step over all the accounts or the specific -// storage in a snapshot which may or may not be composed of multiple layers. -type Iterator interface { - // Next steps the iterator forward one element, returning false if exhausted, - // or an error if iteration failed for some reason (e.g. root being iterated - // becomes stale and garbage collected). - Next() bool - - // Error returns any failure that occurred during iteration, which might have - // caused a premature iteration exit (e.g. layer stack becoming stale). - Error() error - - // Hash returns the hash of the account or storage slot the iterator is - // currently at. - Hash() common.Hash - - // Release releases associated resources. Release should always succeed and - // can be called multiple times without causing error. - Release() -} - -// AccountIterator is an iterator to step over all the accounts in a snapshot, -// which may or may not be composed of multiple layers. -type AccountIterator interface { - Iterator - - // Account returns the RLP encoded slim account the iterator is currently at. - // An error will be returned if the iterator becomes invalid - Account() []byte -} - -// StorageIterator is an iterator to step over the specific storage in a snapshot, -// which may or may not be composed of multiple layers. -type StorageIterator interface { - Iterator - - // Slot returns the storage slot the iterator is currently at. An error will - // be returned if the iterator becomes invalid - Slot() []byte -} +// Type aliases for the iterator interfaces defined in triedb/internal. +type ( + Iterator = internal.Iterator + AccountIterator = internal.AccountIterator + StorageIterator = internal.StorageIterator +) type ( // loadAccount is the function to retrieve the account from the associated diff --git a/triedb/pathdb/verifier.go b/triedb/pathdb/verifier.go index a69b10f4f3..c53590f2fd 100644 --- a/triedb/pathdb/verifier.go +++ b/triedb/pathdb/verifier.go @@ -17,36 +17,15 @@ package pathdb import ( - "encoding/binary" "errors" "fmt" - "math" - "runtime" - "sync" - "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/trie" -) - -// trieKV represents a trie key-value pair -type trieKV struct { - key common.Hash - value []byte -} - -type ( - // trieHasherFn is the interface of trie hasher which can be implemented - // by different trie algorithm. - trieHasherFn func(in chan trieKV, out chan common.Hash) - - // leafCallbackFn is the callback invoked at the leaves of the trie, - // returns the subtrie root with the specified subtrie identifier. - leafCallbackFn func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) + "github.com/ethereum/go-ethereum/triedb/internal" ) // VerifyState traverses the flat states specified by the given state root and @@ -58,7 +37,7 @@ func (db *Database) VerifyState(root common.Hash) error { } defer acctIt.Release() - got, err := generateTrieRoot(acctIt, common.Hash{}, stackTrieHasher, func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) { + got, err := internal.GenerateTrieRoot(nil, "", acctIt, common.Hash{}, stackTrieHasher, func(_ ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) { // Migrate the code first, commit the contract code into the tmp db. if codeHash != types.EmptyCodeHash { code := rawdb.ReadCode(db.diskdb, codeHash) @@ -73,12 +52,12 @@ func (db *Database) VerifyState(root common.Hash) error { } defer storageIt.Release() - hash, err := generateTrieRoot(storageIt, accountHash, stackTrieHasher, nil, stat, false) + hash, err := internal.GenerateTrieRoot(nil, "", storageIt, accountHash, stackTrieHasher, nil, stat, false) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true) + }, internal.NewGenerateStats(), true) if err != nil { return err @@ -89,264 +68,10 @@ func (db *Database) VerifyState(root common.Hash) error { return nil } -// generateStats is a collection of statistics gathered by the trie generator -// for logging purposes. -type generateStats struct { - head common.Hash - start time.Time - - accounts uint64 // Number of accounts done (including those being crawled) - slots uint64 // Number of storage slots done (including those being crawled) - - slotsStart map[common.Hash]time.Time // Start time for account slot crawling - slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled - - lock sync.RWMutex -} - -// newGenerateStats creates a new generator stats. -func newGenerateStats() *generateStats { - return &generateStats{ - slotsStart: make(map[common.Hash]time.Time), - slotsHead: make(map[common.Hash]common.Hash), - start: time.Now(), - } -} - -// progressAccounts updates the generator stats for the account range. -func (stat *generateStats) progressAccounts(account common.Hash, done uint64) { - stat.lock.Lock() - defer stat.lock.Unlock() - - stat.accounts += done - stat.head = account -} - -// finishAccounts updates the generator stats for the finished account range. -func (stat *generateStats) finishAccounts(done uint64) { - stat.lock.Lock() - defer stat.lock.Unlock() - - stat.accounts += done -} - -// progressContract updates the generator stats for a specific in-progress contract. -func (stat *generateStats) progressContract(account common.Hash, slot common.Hash, done uint64) { - stat.lock.Lock() - defer stat.lock.Unlock() - - stat.slots += done - stat.slotsHead[account] = slot - if _, ok := stat.slotsStart[account]; !ok { - stat.slotsStart[account] = time.Now() - } -} - -// finishContract updates the generator stats for a specific just-finished contract. -func (stat *generateStats) finishContract(account common.Hash, done uint64) { - stat.lock.Lock() - defer stat.lock.Unlock() - - stat.slots += done - delete(stat.slotsHead, account) - delete(stat.slotsStart, account) -} - -// report prints the cumulative progress statistic smartly. -func (stat *generateStats) report() { - stat.lock.RLock() - defer stat.lock.RUnlock() - - ctx := []interface{}{ - "accounts", stat.accounts, - "slots", stat.slots, - "elapsed", common.PrettyDuration(time.Since(stat.start)), - } - if stat.accounts > 0 { - // If there's progress on the account trie, estimate the time to finish crawling it - if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 { - var ( - left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts - eta = common.CalculateETA(done, left, time.Since(stat.start)) - ) - // If there are large contract crawls in progress, estimate their finish time - for acc, head := range stat.slotsHead { - start := stat.slotsStart[acc] - if done := binary.BigEndian.Uint64(head[:8]); done > 0 { - left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8]) - - // Override the ETA if larger than the largest until now - if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA { - eta = slotETA - } - } - } - ctx = append(ctx, []interface{}{ - "eta", common.PrettyDuration(eta), - }...) - } - } - log.Info("Iterating state snapshot", ctx...) -} - -// reportDone prints the last log when the whole generation is finished. -func (stat *generateStats) reportDone() { - stat.lock.RLock() - defer stat.lock.RUnlock() - - var ctx []interface{} - ctx = append(ctx, []interface{}{"accounts", stat.accounts}...) - if stat.slots != 0 { - ctx = append(ctx, []interface{}{"slots", stat.slots}...) - } - ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...) - log.Info("Iterated snapshot", ctx...) -} - -// runReport periodically prints the progress information. -func runReport(stats *generateStats, stop chan bool) { - timer := time.NewTimer(0) - defer timer.Stop() - - for { - select { - case <-timer.C: - stats.report() - timer.Reset(time.Second * 8) - case success := <-stop: - if success { - stats.reportDone() - } - return - } - } -} - -// generateTrieRoot generates the trie hash based on the snapshot iterator. -// It can be used for generating account trie, storage trie or even the -// whole state which connects the accounts and the corresponding storages. -func generateTrieRoot(it Iterator, account common.Hash, generatorFn trieHasherFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) { - var ( - in = make(chan trieKV) // chan to pass leaves - out = make(chan common.Hash, 1) // chan to collect result - stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled - wg sync.WaitGroup - ) - // Spin up a go-routine for trie hash re-generation - wg.Add(1) - go func() { - defer wg.Done() - generatorFn(in, out) - }() - // Spin up a go-routine for progress logging - if report && stats != nil { - wg.Add(1) - go func() { - defer wg.Done() - runReport(stats, stoplog) - }() - } - // Create a semaphore to assign tasks and collect results through. We'll pre- - // fill it with nils, thus using the same channel for both limiting concurrent - // processing and gathering results. - threads := runtime.NumCPU() - results := make(chan error, threads) - for i := 0; i < threads; i++ { - results <- nil // fill the semaphore - } - // stop is a helper function to shutdown the background threads - // and return the re-generated trie hash. - stop := func(fail error) (common.Hash, error) { - close(in) - result := <-out - for i := 0; i < threads; i++ { - if err := <-results; err != nil && fail == nil { - fail = err - } - } - stoplog <- fail == nil - - wg.Wait() - return result, fail - } - var ( - logged = time.Now() - processed = uint64(0) - leaf trieKV - ) - // Start to feed leaves - for it.Next() { - if account == (common.Hash{}) { - var ( - err error - fullData []byte - ) - if leafCallback == nil { - fullData, err = types.FullAccountRLP(it.(AccountIterator).Account()) - if err != nil { - return stop(err) - } - } else { - // Wait until the semaphore allows us to continue, aborting if - // a sub-task failed - if err := <-results; err != nil { - results <- nil // stop will drain the results, add a noop back for this error we just consumed - return stop(err) - } - // Fetch the next account and process it concurrently - account, err := types.FullAccount(it.(AccountIterator).Account()) - if err != nil { - return stop(err) - } - go func(hash common.Hash) { - subroot, err := leafCallback(hash, common.BytesToHash(account.CodeHash), stats) - if err != nil { - results <- err - return - } - if account.Root != subroot { - results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot) - return - } - results <- nil - }(it.Hash()) - fullData, err = rlp.EncodeToBytes(account) - if err != nil { - return stop(err) - } - } - leaf = trieKV{it.Hash(), fullData} - } else { - leaf = trieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())} - } - in <- leaf - - // Accumulate the generation statistic if it's required. - processed++ - if time.Since(logged) > 3*time.Second && stats != nil { - if account == (common.Hash{}) { - stats.progressAccounts(it.Hash(), processed) - } else { - stats.progressContract(account, it.Hash(), processed) - } - logged, processed = time.Now(), 0 - } - } - // Commit the last part statistic. - if processed > 0 && stats != nil { - if account == (common.Hash{}) { - stats.finishAccounts(processed) - } else { - stats.finishContract(account, processed) - } - } - return stop(nil) -} - -func stackTrieHasher(in chan trieKV, out chan common.Hash) { +func stackTrieHasher(_ ethdb.KeyValueWriter, _ string, _ common.Hash, in chan internal.TrieKV, out chan common.Hash) { t := trie.NewStackTrie(nil) for leaf := range in { - t.Update(leaf.key[:], leaf.value) + t.Update(leaf.Key[:], leaf.Value) } out <- t.Hash() } From b5d322000cc499d802b2b3768a4eb1a1a5dd1f28 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 7 Apr 2026 20:13:19 +0800 Subject: [PATCH 14/27] eth/protocols/snap: fix block accessList encoding rule (#34644) This PR refactors the encoding rules for `AccessListsPacket` in the wire protocol. Specifically: - The response is now encoded as a list of `rlp.RawValue` - `rlp.EmptyString` is used as a placeholder for unavailable BAL objects --- eth/protocols/snap/handler_test.go | 207 ++++++++++++++++++++++------- eth/protocols/snap/handlers.go | 20 +-- eth/protocols/snap/protocol.go | 4 +- 3 files changed, 175 insertions(+), 56 deletions(-) diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go index 53b22ca7f7..3f6a43a059 100644 --- a/eth/protocols/snap/handler_test.go +++ b/eth/protocols/snap/handler_test.go @@ -18,33 +18,48 @@ package snap import ( "bytes" + "encoding/binary" + "reflect" "testing" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" ) +func makeTestBAL(minSize int) *bal.BlockAccessList { + n := minSize/33 + 1 // 33 bytes per storage read slot in RLP + access := bal.AccountAccess{ + Address: common.HexToAddress("0x01"), + StorageReads: make([][32]byte, n), + } + for i := range access.StorageReads { + binary.BigEndian.PutUint64(access.StorageReads[i][24:], uint64(i)) + } + return &bal.BlockAccessList{Accesses: []bal.AccountAccess{access}} +} + // getChainWithBALs creates a minimal test chain with BALs stored for each block. // It returns the chain, block hashes, and the stored BAL data. func getChainWithBALs(nBlocks int, balSize int) (*core.BlockChain, []common.Hash, []rlp.RawValue) { gspec := &core.Genesis{ - Config: params.TestChainConfig, + Config: params.MergedTestChainConfig, } db := rawdb.NewMemoryDatabase() - _, blocks, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), nBlocks, func(i int, gen *core.BlockGen) {}) + engine := beacon.New(ethash.NewFaker()) + _, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, nBlocks, func(i int, gen *core.BlockGen) {}) options := &core.BlockChainConfig{ - TrieCleanLimit: 0, - TrieDirtyLimit: 0, - TrieTimeLimit: 5 * time.Minute, - NoPrefetch: true, - SnapshotLimit: 0, + StateScheme: rawdb.PathScheme, + TrieTimeLimit: 5 * time.Minute, + NoPrefetch: true, } - bc, err := core.NewBlockChain(db, gspec, ethash.NewFaker(), options) + bc, err := core.NewBlockChain(db, gspec, engine, options) if err != nil { panic(err) } @@ -53,20 +68,22 @@ func getChainWithBALs(nBlocks int, balSize int) (*core.BlockChain, []common.Hash } // Store BALs for each block - var hashes []common.Hash - var bals []rlp.RawValue + var ( + hashes []common.Hash + bals []rlp.RawValue + ) for _, block := range blocks { hash := block.Hash() number := block.NumberU64() - bal := make(rlp.RawValue, balSize) // Fill with data based on block number - for j := range bal { - bal[j] = byte(number + uint64(j)) + bytes, err := rlp.EncodeToBytes(makeTestBAL(balSize)) + if err != nil { + panic(err) } - rawdb.WriteAccessListRLP(db, hash, number, bal) + rawdb.WriteAccessListRLP(db, hash, number, bytes) hashes = append(hashes, hash) - bals = append(bals, bal) + bals = append(bals, bytes) } return bc, hashes, bals } @@ -85,13 +102,18 @@ func TestServiceGetAccessListsQuery(t *testing.T) { result := ServiceGetAccessListsQuery(bc, req) // Verify the results - if len(result) != len(hashes) { - t.Fatalf("expected %d results, got %d", len(hashes), len(result)) + if result.Len() != len(hashes) { + t.Fatalf("expected %d results, got %d", len(hashes), result.Len()) } - for i, bal := range result { - if !bytes.Equal(bal, bals[i]) { - t.Errorf("BAL %d mismatch: got %x, want %x", i, bal, bals[i]) + var ( + index int + it = result.ContentIterator() + ) + for it.Next() { + if !bytes.Equal(it.Value(), bals[index]) { + t.Errorf("BAL %d mismatch: got %x, want %x", index, it.Value(), bals[index]) } + index++ } } @@ -111,25 +133,23 @@ func TestServiceGetAccessListsQueryEmpty(t *testing.T) { result := ServiceGetAccessListsQuery(bc, req) // Verify length - if len(result) != len(mixed) { - t.Fatalf("expected %d results, got %d", len(mixed), len(result)) + if result.Len() != len(mixed) { + t.Fatalf("expected %d results, got %d", len(mixed), result.Len()) } // Check positional correspondence - if !bytes.Equal(result[0], bals[0]) { - t.Errorf("index 0: expected known BAL, got %x", result[0]) + var expectVal = []rlp.RawValue{ + bals[0], rlp.EmptyString, bals[1], rlp.EmptyString, bals[2], } - if result[1] != nil { - t.Errorf("index 1: expected nil for unknown hash, got %x", result[1]) - } - if !bytes.Equal(result[2], bals[1]) { - t.Errorf("index 2: expected known BAL, got %x", result[2]) - } - if result[3] != nil { - t.Errorf("index 3: expected nil for unknown hash, got %x", result[3]) - } - if !bytes.Equal(result[4], bals[2]) { - t.Errorf("index 4: expected known BAL, got %x", result[4]) + var ( + index int + it = result.ContentIterator() + ) + for it.Next() { + if !bytes.Equal(it.Value(), expectVal[index]) { + t.Errorf("BAL %d mismatch: got %x, want %x", index, it.Value(), expectVal[index]) + } + index++ } } @@ -154,8 +174,8 @@ func TestServiceGetAccessListsQueryCap(t *testing.T) { result := ServiceGetAccessListsQuery(bc, req) // Can't get more than maxAccessListLookups results - if len(result) > maxAccessListLookups { - t.Fatalf("expected at most %d results, got %d", maxAccessListLookups, len(result)) + if result.Len() > maxAccessListLookups { + t.Fatalf("expected at most %d results, got %d", maxAccessListLookups, result.Len()) } } @@ -179,21 +199,116 @@ func TestServiceGetAccessListsQueryByteLimit(t *testing.T) { result := ServiceGetAccessListsQuery(bc, req) // Should have stopped before returning all blocks - if len(result) >= nBlocks { - t.Fatalf("expected fewer than %d results due to byte limit, got %d", nBlocks, len(result)) + if result.Len() >= nBlocks { + t.Fatalf("expected fewer than %d results due to byte limit, got %d", nBlocks, result.Len()) } // Should have returned at least one - if len(result) == 0 { + if result.Len() == 0 { t.Fatal("expected at least one result") } // The total size should exceed the limit (the entry that crosses it is included) - var total uint64 - for _, bal := range result { - total += uint64(len(bal)) - } - if total <= softResponseLimit { - t.Errorf("total response size %d should exceed soft limit %d (includes one entry past limit)", total, softResponseLimit) + if result.Size() <= softResponseLimit { + t.Errorf("total response size %d should exceed soft limit %d (includes one entry past limit)", result.Size(), softResponseLimit) + } +} + +// TestGetAccessListResponseDecoding verifies that an AccessListsPacket +// round-trips through RLP encode/decode, preserving positional +// correspondence and correctly representing absent BALs as empty strings. +func TestGetAccessListResponseDecoding(t *testing.T) { + t.Parallel() + + // Build two real BALs of different sizes. + bal1 := makeTestBAL(100) + bal2 := makeTestBAL(200) + bytes1, _ := rlp.EncodeToBytes(bal1) + bytes2, _ := rlp.EncodeToBytes(bal2) + + tests := []struct { + name string + items []rlp.RawValue // nil entry = unavailable BAL + counts int // expected decoded length + }{ + { + name: "all present", + items: []rlp.RawValue{bytes1, bytes2}, + counts: 2, + }, + { + name: "all absent", + items: []rlp.RawValue{rlp.EmptyString, rlp.EmptyString, rlp.EmptyString}, + counts: 3, + }, + { + name: "mixed present and absent", + items: []rlp.RawValue{bytes1, rlp.EmptyString, bytes2, rlp.EmptyString}, + counts: 4, + }, + { + name: "empty response", + items: []rlp.RawValue{}, + counts: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build the packet using Append. + var orig AccessListsPacket + orig.ID = 42 + for _, item := range tt.items { + if err := orig.AccessLists.AppendRaw(item); err != nil { + t.Fatalf("AppendRaw failed: %v", err) + } + } + + // Encode -> Decode round-trip. + enc, err := rlp.EncodeToBytes(&orig) + if err != nil { + t.Fatalf("encode failed: %v", err) + } + var dec AccessListsPacket + if err := rlp.DecodeBytes(enc, &dec); err != nil { + t.Fatalf("decode failed: %v", err) + } + + // Verify ID preserved. + if dec.ID != orig.ID { + t.Fatalf("ID mismatch: got %d, want %d", dec.ID, orig.ID) + } + + // Verify element count. + if dec.AccessLists.Len() != tt.counts { + t.Fatalf("length mismatch: got %d, want %d", dec.AccessLists.Len(), tt.counts) + } + + // Verify each element positionally. + it := dec.AccessLists.ContentIterator() + for i, want := range tt.items { + if !it.Next() { + t.Fatalf("iterator exhausted at index %d", i) + } + got := it.Value() + if !bytes.Equal(got, want) { + t.Errorf("element %d: got %x, want %x", i, got, want) + } + if !bytes.Equal(got, rlp.EmptyString) { + obj := new(bal.BlockAccessList) + if err := rlp.DecodeBytes(got, obj); err != nil { + t.Fatalf("decode failed: %v", err) + } + if bytes.Equal(got, bytes1) && !reflect.DeepEqual(obj, bal1) { + t.Fatalf("decode failed: got %x, want %x", obj, bal1) + } + if bytes.Equal(got, bytes2) && !reflect.DeepEqual(obj, bal2) { + t.Fatalf("decode failed: got %x, want %x", obj, bal2) + } + } + } + if it.Next() { + t.Error("iterator has extra elements after expected end") + } + }) } } diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go index 4d60aab1f6..5a5733bdb4 100644 --- a/eth/protocols/snap/handlers.go +++ b/eth/protocols/snap/handlers.go @@ -559,16 +559,15 @@ func handleGetAccessLists(backend Backend, msg Decoder, peer *Peer) error { if err := msg.Decode(&req); err != nil { return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } - bals := ServiceGetAccessListsQuery(backend.Chain(), &req) return p2p.Send(peer.rw, AccessListsMsg, &AccessListsPacket{ ID: req.ID, - AccessLists: bals, + AccessLists: ServiceGetAccessListsQuery(backend.Chain(), &req), }) } // ServiceGetAccessListsQuery assembles the response to an access list query. // It is exposed to allow external packages to test protocol behavior. -func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacket) []rlp.RawValue { +func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacket) rlp.RawList[rlp.RawValue] { if req.Bytes > softResponseLimit { req.Bytes = softResponseLimit } @@ -577,20 +576,25 @@ func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacke req.Hashes = req.Hashes[:maxAccessListLookups] } var ( - bals []rlp.RawValue - bytes uint64 + err error + bytes uint64 + response = rlp.RawList[rlp.RawValue]{} ) for _, hash := range req.Hashes { if bal := chain.GetAccessListRLP(hash); len(bal) > 0 { - bals = append(bals, bal) + err = response.AppendRaw(bal) bytes += uint64(len(bal)) } else { // Either the block is unknown or the BAL doesn't exist - bals = append(bals, nil) + err = response.AppendRaw(rlp.EmptyString) + bytes += 1 + } + if err != nil { + break } if bytes > req.Bytes { break } } - return bals + return response } diff --git a/eth/protocols/snap/protocol.go b/eth/protocols/snap/protocol.go index 7913f8b053..685f468da3 100644 --- a/eth/protocols/snap/protocol.go +++ b/eth/protocols/snap/protocol.go @@ -229,8 +229,8 @@ type GetAccessListsPacket struct { // Each entry corresponds to the requested hash at the same index. // Empty entries indicate the BAL is unavailable. type AccessListsPacket struct { - ID uint64 // ID of the request this is a response for - AccessLists []rlp.RawValue // Requested BALs + ID uint64 // ID of the request this is a response for + AccessLists rlp.RawList[rlp.RawValue] // Requested BALs } func (*GetAccountRangePacket) Name() string { return "GetAccountRange" } From 52b8c09fdfe2a6cc6efc29c87186778a5260ba17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20L=C3=B3pez=20Le=C3=B3n?= Date: Tue, 7 Apr 2026 10:31:41 -0300 Subject: [PATCH 15/27] triedb/pathdb: skip duplicate-root layer insertion (#34642) PathDB keys diff layers by state root, not by block hash. That means a side-chain block can legitimately collide with an existing canonical diff layer when both blocks produce the same post-state (for example same parent, same coinbase, no txs). Today `layerTree.add` blindly inserts that second layer. If the root already exists, this overwrites `tree.layers[root]` and appends the same root to the mutation lookup again. Later account/storage lookups resolve that root to the wrong diff layer, which can corrupt reads for descendant canonical states. At runtime, the corruption is silent: no error is logged and no invariant check fires. State reads against affected descendants simply return stale data from the wrong diff layer (for example, an account balance that reflects one fewer block reward), which can propagate into RPC responses and block validation. This change makes duplicate-root inserts idempotent. A second layer with the same state root does not add any new retrievable state to a tree that is already keyed by root; keeping the original layer preserves the existing parent chain and avoids polluting the lookup history with duplicate roots. The regression test imports a canonical chain of two layers followed by a fork layer at height 1 with the same state root but a different block hash. Before the fix, account and storage lookups at the head resolve the fork layer instead of the canonical one. After the fix, the duplicate insert is skipped and lookups remain correct. --- triedb/pathdb/layertree.go | 9 +++++++++ triedb/pathdb/layertree_test.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index ec45257db5..0d7ca5a5b4 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -151,6 +151,15 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6 if root == parentRoot { return errors.New("layer cycle") } + // If a layer with this root already exists, skip the insertion. Fork blocks + // can produce the same state root as the canonical block (same parent, same + // coinbase, zero txs); overwriting tree.layers[root] would corrupt the parent + // chain for any child layers already built on top of the existing one, and + // appending a duplicate root to the lookup indices causes accountTip/storageTip + // to resolve the wrong layer. + if tree.get(root) != nil { + return nil + } parent := tree.get(parentRoot) if parent == nil { return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot) diff --git a/triedb/pathdb/layertree_test.go b/triedb/pathdb/layertree_test.go index a74c6eb045..285ca67b6c 100644 --- a/triedb/pathdb/layertree_test.go +++ b/triedb/pathdb/layertree_test.go @@ -575,6 +575,40 @@ func TestDescendant(t *testing.T) { } } +func TestDuplicateRootLookup(t *testing.T) { + // Chain: + // C1->C2->C3 (HEAD) + tr := newTestLayerTree() // base = 0x1 + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false)) + + // A fork block with the same state root as C2; inserting it must not + // pollute the lookup history for the canonical descendant C3. + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false)) + if n := tr.len(); n != 3 { + t.Fatalf("duplicate root insert changed layer count, got %d, want 3", n) + } + + l, err := tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3}) + if err != nil { + t.Fatalf("account lookup failed: %v", err) + } + if l.rootHash() != (common.Hash{0x3}) { + t.Errorf("unexpected account tip, want %x, got %x", common.Hash{0x3}, l.rootHash()) + } + + l, err = tr.lookupStorage(common.HexToHash("0xa"), common.HexToHash("0x1"), common.Hash{0x3}) + if err != nil { + t.Fatalf("storage lookup failed: %v", err) + } + if l.rootHash() != (common.Hash{0x3}) { + t.Errorf("unexpected storage tip, want %x, got %x", common.Hash{0x3}, l.rootHash()) + } +} + func TestAccountLookup(t *testing.T) { // Chain: // C1->C2->C3->C4 (HEAD) From 0bafb29490751ad807ca1433962d5aef5ae27986 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 7 Apr 2026 22:04:07 +0800 Subject: [PATCH 16/27] core/types: add accessList to WithSeal and WithBody (#34651) Co-authored-by: Felix Lange --- core/types/block.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/types/block.go b/core/types/block.go index 60c83c9db1..ea576ed232 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -505,6 +505,7 @@ func (b *Block) WithSeal(header *Header) *Block { transactions: b.transactions, uncles: b.uncles, withdrawals: b.withdrawals, + accessList: b.accessList, } } @@ -516,6 +517,7 @@ func (b *Block) WithBody(body Body) *Block { transactions: slices.Clone(body.Transactions), uncles: make([]*Header, len(body.Uncles)), withdrawals: slices.Clone(body.Withdrawals), + accessList: b.accessList, } for i := range body.Uncles { block.uncles[i] = CopyHeader(body.Uncles[i]) From 9878ef926db2fb45d33a9331feff1e38221ad267 Mon Sep 17 00:00:00 2001 From: locoholy <68823405+locoholy@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:01:26 +0500 Subject: [PATCH 17/27] ethclient: omit empty address/topics fields in RPC filter requests (#33884) Changes JSON serialization of FilterCriteria to exclude "address" when it is empty. --- ethclient/ethclient.go | 6 +++++- ethclient/types_test.go | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 85286ec919..412f8955ba 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -498,7 +498,11 @@ func (ec *Client) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuer func toFilterArg(q ethereum.FilterQuery) (interface{}, error) { arg := map[string]interface{}{} - if q.Addresses != nil { + // Only include "address" when there are actual address filters. + // An empty slice is treated the same as nil (no filter), and omitting + // the field avoids sending "address":[] to nodes that reject empty arrays + // (e.g. Hedera, some non-Geth implementations). + if len(q.Addresses) > 0 { arg["address"] = q.Addresses } if q.Topics != nil { diff --git a/ethclient/types_test.go b/ethclient/types_test.go index dcb9a579b7..8820b11162 100644 --- a/ethclient/types_test.go +++ b/ethclient/types_test.go @@ -53,6 +53,22 @@ func TestToFilterArg(t *testing.T) { }, nil, }, + { + // empty Addresses slice must be treated same as nil: + // the "address" field must be omitted so that non-Geth nodes + // (e.g. Hedera) do not reject the request with an error. + "with empty addresses slice", + ethereum.FilterQuery{ + Addresses: []common.Address{}, + FromBlock: big.NewInt(1), + ToBlock: big.NewInt(2), + }, + map[string]interface{}{ + "fromBlock": "0x1", + "toBlock": "0x2", + }, + nil, + }, { "without BlockHash", ethereum.FilterQuery{ From 04e40995d9898ae84e11ba6ec2354c1a4d5e5ee9 Mon Sep 17 00:00:00 2001 From: DELENE-TCHIO <119831304+DELENE-TCHIO-ROMUALD@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:55:09 +0100 Subject: [PATCH 18/27] core: merge access events for all system calls (#34637) ProcessBeaconBlockRoot (EIP-4788) and processRequestsSystemCall (EIP-7002/7251) do not merge the EVM access events into the state after execution. ProcessParentBlockHash (EIP-2935) already does this correctly at line 290-291. Without this merge, the Verkle witness will be missing the storage accesses from the beacon root and request system calls, leading to incomplete witnesses and potential consensus issues when Verkle activates. --- core/state_processor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/state_processor.go b/core/state_processor.go index 85f106d58c..bbb1341299 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -260,6 +260,9 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) + if evm.StateDB.AccessEvents() != nil { + evm.StateDB.AccessEvents().Merge(evm.AccessEvents) + } evm.StateDB.Finalise(true) } @@ -323,6 +326,9 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) + if evm.StateDB.AccessEvents() != nil { + evm.StateDB.AccessEvents().Merge(evm.AccessEvents) + } evm.StateDB.Finalise(true) if err != nil { return fmt.Errorf("system call failed to execute: %v", err) From a8ea6319f1551056205c6c436b9b0e0a1fbbef5e Mon Sep 17 00:00:00 2001 From: Mael Regnery Date: Wed, 8 Apr 2026 12:57:29 +0200 Subject: [PATCH 19/27] eth/filters: return -32602 when exceeding the block range limit (#34647) Co-authored-by: Felix Lange --- eth/filters/filter.go | 3 +-- eth/filters/filter_test.go | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/eth/filters/filter.go b/eth/filters/filter.go index 04e11f0475..f31b9568cd 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -19,7 +19,6 @@ package filters import ( "context" "errors" - "fmt" "math" "math/big" "slices" @@ -147,7 +146,7 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) { return nil, err } if f.rangeLimit != 0 && (end-begin) > f.rangeLimit { - return nil, fmt.Errorf("exceed maximum block range: %d", f.rangeLimit) + return nil, invalidParamsErr("exceed maximum block range %d", f.rangeLimit) } return f.rangeLogs(ctx, begin, end) } diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go index e7b1b08046..c133438c64 100644 --- a/eth/filters/filter_test.go +++ b/eth/filters/filter_test.go @@ -19,6 +19,7 @@ package filters import ( "context" "encoding/json" + "errors" "math/big" "strings" "testing" @@ -634,7 +635,19 @@ func TestRangeLimit(t *testing.T) { // Set rangeLimit to 5, but request a range of 9 (end - begin = 9, from 0 to 9) filter := sys.NewRangeFilter(0, 9, nil, nil, 5) _, err = filter.Logs(context.Background()) - if err == nil || !strings.Contains(err.Error(), "exceed maximum block range") { - t.Fatalf("expected range limit error, got %v", err) + if err == nil { + t.Fatal("expected range limit error, got nil") + } + + var re rpc.Error + if errors.As(err, &re) { + if re.ErrorCode() != -32602 { + t.Fatalf("expected error code -32602, got %d", re.ErrorCode()) + } + if re.Error() != "exceed maximum block range 5" { + t.Fatalf("expected error message 'exceed maximum block range 5', got %q", re.Error()) + } + } else { + t.Fatalf("expected rpc error, got %v", err) } } From 21b19362c27762e2a1922e6a0f619c299b62d1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Thu, 9 Apr 2026 03:12:35 +0200 Subject: [PATCH 20/27] core/state: fix tracer hook for EIP-7708 burn logs (#34688) This PR fixes https://github.com/ethereum/go-ethereum/issues/34623 by changing the `vm.StateDB` interface: Instead of `EmitLogsForBurnAccounts()` emitting burn logs, `LogsForBurnAccounts() []*types.Log` just returns these logs which are then emitted by the caller. This way when tracing is used, `hookedStateDB.AddLog` will be used automatically and there is no need to duplicate either the burn log logic or the `OnLog` tracing hook. --- core/state/statedb.go | 19 +++++++++++-------- core/state/statedb_hooked.go | 4 ++-- core/state_transition.go | 4 +++- core/vm/interface.go | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 854aaf6109..8b09ea89f6 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -745,7 +745,7 @@ type removedAccountWithBalance struct { balance *uint256.Int } -// EmitLogsForBurnAccounts emits the eth burn logs for accounts scheduled for +// LogsForBurnAccounts returns the eth burn logs for accounts scheduled for // removal which still have positive balance. The purpose of this function is // to handle a corner case of EIP-7708 where a self-destructed account might // still receive funds between sending/burning its previous balance and actual @@ -755,7 +755,7 @@ type removedAccountWithBalance struct { // // This function should only be invoked at the transaction boundary, specifically // before the Finalise. -func (s *StateDB) EmitLogsForBurnAccounts() { +func (s *StateDB) LogsForBurnAccounts() []*types.Log { var list []removedAccountWithBalance for addr := range s.journal.dirties { if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() { @@ -765,14 +765,17 @@ func (s *StateDB) EmitLogsForBurnAccounts() { }) } } - if list != nil { - sort.Slice(list, func(i, j int) bool { - return list[i].address.Cmp(list[j].address) < 0 - }) + if list == nil { + return nil } - for _, acct := range list { - s.AddLog(types.EthBurnLog(acct.address, acct.balance)) + sort.Slice(list, func(i, j int) bool { + return list[i].address.Cmp(list[j].address) < 0 + }) + logs := make([]*types.Log, len(list)) + for i, acct := range list { + logs[i] = types.EthBurnLog(acct.address, acct.balance) } + return logs } // Finalise finalises the state by removing the destructed objects and clears diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 8c217fba48..52cf98d19b 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -229,8 +229,8 @@ func (s *hookedStateDB) AddLog(log *types.Log) { } } -func (s *hookedStateDB) EmitLogsForBurnAccounts() { - s.inner.EmitLogsForBurnAccounts() +func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { + return s.inner.LogsForBurnAccounts() } func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { diff --git a/core/state_transition.go b/core/state_transition.go index 52375bedaa..bd7e5daeff 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -584,7 +584,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } if rules.IsAmsterdam { - st.evm.StateDB.EmitLogsForBurnAccounts() + for _, log := range st.evm.StateDB.LogsForBurnAccounts() { + st.evm.StateDB.AddLog(log) + } } return &ExecutionResult{ UsedGas: st.gasUsed(), diff --git a/core/vm/interface.go b/core/vm/interface.go index 6a93846ac5..d7c4340e06 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -87,7 +87,7 @@ type StateDB interface { Snapshot() int AddLog(*types.Log) - EmitLogsForBurnAccounts() + LogsForBurnAccounts() []*types.Log AddPreimage(common.Hash, []byte) Witness() *stateless.Witness From 68c7058a80859dc5de75d3e34708cd1ea2f094e5 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Thu, 9 Apr 2026 03:19:54 +0200 Subject: [PATCH 21/27] core/stateless: fix parsing an empty witness (#34683) This is to fix a crasher in keeper. --- core/stateless/encoding.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/stateless/encoding.go b/core/stateless/encoding.go index d559178892..1b20c4cb2a 100644 --- a/core/stateless/encoding.go +++ b/core/stateless/encoding.go @@ -17,6 +17,7 @@ package stateless import ( + "errors" "io" "github.com/ethereum/go-ethereum/common/hexutil" @@ -42,6 +43,9 @@ func (w *Witness) ToExtWitness() *ExtWitness { // FromExtWitness converts the consensus witness format into our internal one. func (w *Witness) FromExtWitness(ext *ExtWitness) error { + if len(ext.Headers) == 0 { + return errors.New("witness must contain at least one header") + } w.Headers = ext.Headers w.Codes = make(map[string]struct{}, len(ext.Codes)) From 3772bb536a67a2f17ed98244775eb16698f79140 Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:39:38 +0200 Subject: [PATCH 22/27] triedb/pathdb: fix lookup sentinel collision with zero disk layer root (#34680) --- triedb/pathdb/layertree.go | 8 +-- triedb/pathdb/layertree_test.go | 115 ++++++++++++++++++++++++++++++++ triedb/pathdb/lookup.go | 37 +++++----- 3 files changed, 137 insertions(+), 23 deletions(-) diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index 0d7ca5a5b4..b20e40bd05 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -319,8 +319,8 @@ func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) tree.lock.RLock() defer tree.lock.RUnlock() - tip := tree.lookup.accountTip(accountHash, state, tree.base.root) - if tip == (common.Hash{}) { + tip, ok := tree.lookup.accountTip(accountHash, state, tree.base.root) + if !ok { return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale) } l := tree.layers[tip] @@ -337,8 +337,8 @@ func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Ha tree.lock.RLock() defer tree.lock.RUnlock() - tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root) - if tip == (common.Hash{}) { + tip, ok := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root) + if !ok { return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale) } l := tree.layers[tip] diff --git a/triedb/pathdb/layertree_test.go b/triedb/pathdb/layertree_test.go index 285ca67b6c..82eb182990 100644 --- a/triedb/pathdb/layertree_test.go +++ b/triedb/pathdb/layertree_test.go @@ -916,3 +916,118 @@ func TestStorageLookup(t *testing.T) { } } } + +// TestLookupZeroBaseRootFallback is a regression test for a sentinel +// collision in accountTip/storageTip: before the fix they returned +// common.Hash{} as both the "stale" marker and the disk-layer fallback +// when the disk root itself happened to be zero. lookupAccount/Storage +// then misreported a legitimate fallback as errSnapshotStale. +// +// On the merkle path the collision was invisible because the empty +// merkle trie hashes to types.EmptyRootHash (a concrete non-zero +// keccak), so the disk layer's root was never the zero hash in +// practice. The bug only surfaces once the disk layer root can +// legitimately be zero (for example a fresh verkle/bintrie database +// where the empty binary trie hashes to EmptyVerkleHash == +// common.Hash{}). +// +// The test constructs a layer tree whose base layer's root IS the zero +// hash, stacks diff layers on top, and exercises four cases: +// +// 1. Look up an account NEVER written → should fall through to the +// disk layer and return (diskLayer, nil). Before the fix this +// returned errSnapshotStale because the fallback hash collided +// with the sentinel. +// 2. Symmetric case for lookupStorage. +// 3. Look up an account written in a diff layer → should return that +// diff layer (the normal happy path is unaffected by the fix). +// 4. Look up any key at a state root that isn't part of the tree +// (neither the disk root nor a descendant of it) → MUST still +// return errSnapshotStale. This pins the "other half" of the +// contract so a future refactor that always returns ok=true would +// fail here. +func TestLookupZeroBaseRootFallback(t *testing.T) { + // Build a layer tree whose disk-layer root is common.Hash{} — + // mirrors the bintrie/verkle configuration where the empty trie + // hashes to EmptyVerkleHash. newTestLayerTree can't be reused + // because it hard-codes common.Hash{0x1}. + db := New(rawdb.NewMemoryDatabase(), nil, false) + base := newDiskLayer(common.Hash{}, 0, db, nil, nil, newBuffer(0, nil, nil, 0), nil) + tr := newLayerTree(base) + + // Stack two diff layers on the zero-rooted disk layer, each + // touching a known account and slot so we have something for the + // happy-path lookups to find later. + if err := tr.add( + common.Hash{0x2}, common.Hash{}, + 1, + NewNodeSetWithOrigin(nil, nil), + NewStateSetWithOrigin( + randomAccountSet("0xa"), + randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), + nil, nil, false), + ); err != nil { + t.Fatalf("add first diff layer: %v", err) + } + if err := tr.add( + common.Hash{0x3}, common.Hash{0x2}, + 2, + NewNodeSetWithOrigin(nil, nil), + NewStateSetWithOrigin( + randomAccountSet("0xb"), + nil, nil, nil, false), + ); err != nil { + t.Fatalf("add second diff layer: %v", err) + } + + // Case 1: unknown account queried at the head. The lookup must + // fall through the diff layers, hit the disk-layer fallback at + // base=common.Hash{}, and return the disk layer with no error — + // NOT errSnapshotStale. + l, err := tr.lookupAccount(common.HexToHash("0xdead"), common.Hash{0x3}) + if err != nil { + t.Fatalf("lookupAccount on zero-base disk layer: unexpected error %v", err) + } + if l.rootHash() != (common.Hash{}) { + t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash()) + } + + // Case 2: symmetric check for storage. Slot 0x99 was never written, + // so the lookup must fall through to the disk layer just like + // Case 1. + l, err = tr.lookupStorage( + common.HexToHash("0xdead"), common.HexToHash("0x99"), common.Hash{0x3}) + if err != nil { + t.Fatalf("lookupStorage on zero-base disk layer: unexpected error %v", err) + } + if l.rootHash() != (common.Hash{}) { + t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash()) + } + + // Case 3: happy path. Account 0xa was written at diff layer 0x2. + // The lookup must return that layer, proving the fix didn't break + // the normal resolution path. + l, err = tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3}) + if err != nil { + t.Fatalf("lookupAccount(known): %v", err) + } + if l.rootHash() != (common.Hash{0x2}) { + t.Errorf("known account tip: want %x, got %x", + common.Hash{0x2}, l.rootHash()) + } + + // Case 4: truly stale state root. This pins the other half of the + // contract — the boolean must actually signal not-found for an + // unknown state, otherwise a refactor that always returned + // ok=true would still pass cases 1–3. + _, err = tr.lookupAccount(common.HexToHash("0xa"), common.HexToHash("0xdeadbeef")) + if !errors.Is(err, errSnapshotStale) { + t.Errorf("lookupAccount(stale state): want errSnapshotStale, got %v", err) + } + _, err = tr.lookupStorage( + common.HexToHash("0xa"), common.HexToHash("0x1"), + common.HexToHash("0xdeadbeef")) + if !errors.Is(err, errSnapshotStale) { + t.Errorf("lookupStorage(stale state): want errSnapshotStale, got %v", err) + } +} diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index 719546f410..9b300ec871 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -92,12 +92,16 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha // stateID or is a descendant of it. // // If found, the account data corresponding to the supplied stateID resides -// in that layer. Otherwise, two scenarios are possible: +// in the layer identified by the returned hash (ok=true). Otherwise, +// (common.Hash{}, false) is returned to signal that the supplied stateID is +// stale. // -// (a) the account remains unmodified from the current disk layer up to the state -// layer specified by the stateID: fallback to the disk layer for data retrieval, -// (b) or the layer specified by the stateID is stale: reject the data retrieval. -func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { +// Note the returned hash may itself be common.Hash{} when the disk layer's +// root is zero — as is the case for a fresh verkle/bintrie database whose +// empty trie hashes to EmptyVerkleHash. Callers must therefore consult the +// boolean rather than comparing the returned hash against common.Hash{} +// directly. +func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) { // Traverse the mutation history from latest to oldest one. Several // scenarios are possible: // @@ -123,31 +127,26 @@ func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base c // containing the modified data. Otherwise, the current state may be ahead // of the requested one or belong to a different branch. if list[i] == stateID || l.descendant(stateID, list[i]) { - return list[i] + return list[i], true } } // No layer matching the stateID or its descendants was found. Use the // current disk layer as a fallback. if base == stateID || l.descendant(stateID, base) { - return base + return base, true } // The layer associated with 'stateID' is not the descendant of the current // disk layer, it's already stale, return nothing. - return common.Hash{} + return common.Hash{}, false } // storageTip traverses the layer list associated with the given account and // slot hash in reverse order to locate the first entry that either matches // the specified stateID or is a descendant of it. // -// If found, the storage data corresponding to the supplied stateID resides -// in that layer. Otherwise, two scenarios are possible: -// -// (a) the storage slot remains unmodified from the current disk layer up to -// the state layer specified by the stateID: fallback to the disk layer for -// data retrieval, (b) or the layer specified by the stateID is stale: reject -// the data retrieval. -func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { +// See accountTip for the returned-hash / ok convention — the same +// bintrie-zero-root caveat applies here. +func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) { list := l.storages[storageKey(accountHash, slotHash)] for i := len(list) - 1; i >= 0; i-- { // If the current state matches the stateID, or the requested state is a @@ -155,17 +154,17 @@ func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, state // containing the modified data. Otherwise, the current state may be ahead // of the requested one or belong to a different branch. if list[i] == stateID || l.descendant(stateID, list[i]) { - return list[i] + return list[i], true } } // No layer matching the stateID or its descendants was found. Use the // current disk layer as a fallback. if base == stateID || l.descendant(stateID, base) { - return base + return base, true } // The layer associated with 'stateID' is not the descendant of the current // disk layer, it's already stale, return nothing. - return common.Hash{} + return common.Hash{}, false } // addLayer traverses the state data retained in the specified diff layer and From 58557cb4635d4e6f3e49fcdc82a6469554e929a6 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:27:19 +0200 Subject: [PATCH 23/27] cmd/geth: add subcommand for offline binary tree conversion (#33740) This tool is designed for the offline translation of an MPT database to a binary trie. This is to be used for users who e.g. want to prove equivalence of a binary tree chain shadowing the MPT chain. It adds a `bintrie` command, cleanly separating the concerns. --- cmd/geth/bintrie_convert.go | 408 +++++++++++++++++++++++++++++++ cmd/geth/bintrie_convert_test.go | 229 +++++++++++++++++ cmd/geth/main.go | 2 + 3 files changed, 639 insertions(+) create mode 100644 cmd/geth/bintrie_convert.go create mode 100644 cmd/geth/bintrie_convert_test.go diff --git a/cmd/geth/bintrie_convert.go b/cmd/geth/bintrie_convert.go new file mode 100644 index 0000000000..3730768697 --- /dev/null +++ b/cmd/geth/bintrie_convert.go @@ -0,0 +1,408 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "errors" + "fmt" + "runtime" + "runtime/debug" + "slices" + "time" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/trie/bintrie" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/pathdb" + "github.com/urfave/cli/v2" +) + +var ( + deleteSourceFlag = &cli.BoolFlag{ + Name: "delete-source", + Usage: "Delete MPT trie nodes after conversion", + } + memoryLimitFlag = &cli.Uint64Flag{ + Name: "memory-limit", + Usage: "Max heap allocation in MB before forcing a commit cycle", + Value: 16384, + } + + bintrieCommand = &cli.Command{ + Name: "bintrie", + Usage: "A set of commands for binary trie operations", + Description: "", + Subcommands: []*cli.Command{ + { + Name: "convert", + Usage: "Convert MPT state to binary trie", + ArgsUsage: "[state-root]", + Action: convertToBinaryTrie, + Flags: slices.Concat([]cli.Flag{ + deleteSourceFlag, + memoryLimitFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), + Description: ` +geth bintrie convert [--delete-source] [--memory-limit MB] [state-root] + +Reads all state from the Merkle Patricia Trie and writes it into a Binary Trie, +operating offline. Memory-safe via periodic commit-and-reload cycles. + +The optional state-root argument specifies which state root to convert. +If omitted, the head block's state root is used. + +Flags: + --delete-source Delete MPT trie nodes after successful conversion + --memory-limit Max heap allocation in MB before forcing a commit (default: 16384) +`, + }, + }, + } +) + +type conversionStats struct { + accounts uint64 + slots uint64 + codes uint64 + commits uint64 + start time.Time + lastReport time.Time + lastMemChk time.Time +} + +func (s *conversionStats) report(force bool) { + if !force && time.Since(s.lastReport) < 8*time.Second { + return + } + elapsed := time.Since(s.start).Seconds() + acctRate := float64(0) + if elapsed > 0 { + acctRate = float64(s.accounts) / elapsed + } + log.Info("Conversion progress", + "accounts", s.accounts, + "slots", s.slots, + "codes", s.codes, + "commits", s.commits, + "accounts/sec", fmt.Sprintf("%.0f", acctRate), + "elapsed", common.PrettyDuration(time.Since(s.start)), + ) + s.lastReport = time.Now() +} + +func convertToBinaryTrie(ctx *cli.Context) error { + if ctx.NArg() > 1 { + return errors.New("too many arguments") + } + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + chaindb := utils.MakeChainDatabase(ctx, stack, false) + defer chaindb.Close() + + headBlock := rawdb.ReadHeadBlock(chaindb) + if headBlock == nil { + return errors.New("no head block found") + } + var ( + root common.Hash + err error + ) + if ctx.NArg() == 1 { + root, err = parseRoot(ctx.Args().First()) + if err != nil { + return fmt.Errorf("invalid state root: %w", err) + } + } else { + root = headBlock.Root() + } + log.Info("Starting MPT to binary trie conversion", "root", root, "block", headBlock.NumberU64()) + + srcTriedb := utils.MakeTrieDatabase(ctx, stack, chaindb, true, true, false) + defer srcTriedb.Close() + + destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{ + IsVerkle: true, + PathDB: &pathdb.Config{ + JournalDirectory: stack.ResolvePath("triedb-bintrie"), + }, + }) + defer destTriedb.Close() + + binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + if err != nil { + return fmt.Errorf("failed to create binary trie: %w", err) + } + memLimit := ctx.Uint64(memoryLimitFlag.Name) * 1024 * 1024 + + currentRoot, err := runConversionLoop(chaindb, srcTriedb, destTriedb, binTrie, root, memLimit) + if err != nil { + return err + } + log.Info("Conversion complete", "binaryRoot", currentRoot) + + if ctx.Bool(deleteSourceFlag.Name) { + log.Info("Deleting source MPT data") + if err := deleteMPTData(chaindb, srcTriedb, root); err != nil { + return fmt.Errorf("MPT deletion failed: %w", err) + } + log.Info("Source MPT data deleted") + } + return nil +} + +func runConversionLoop(chaindb ethdb.Database, srcTriedb *triedb.Database, destTriedb *triedb.Database, binTrie *bintrie.BinaryTrie, root common.Hash, memLimit uint64) (common.Hash, error) { + currentRoot := types.EmptyBinaryHash + stats := &conversionStats{ + start: time.Now(), + lastReport: time.Now(), + lastMemChk: time.Now(), + } + + srcTrie, err := trie.NewStateTrie(trie.StateTrieID(root), srcTriedb) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to open source trie: %w", err) + } + acctIt, err := srcTrie.NodeIterator(nil) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to create account iterator: %w", err) + } + accIter := trie.NewIterator(acctIt) + + for accIter.Next() { + var acc types.StateAccount + if err := rlp.DecodeBytes(accIter.Value, &acc); err != nil { + return common.Hash{}, fmt.Errorf("invalid account RLP: %w", err) + } + addrBytes := srcTrie.GetKey(accIter.Key) + if addrBytes == nil { + return common.Hash{}, fmt.Errorf("missing preimage for account hash %x (run with --cache.preimages)", accIter.Key) + } + addr := common.BytesToAddress(addrBytes) + + var code []byte + codeHash := common.BytesToHash(acc.CodeHash) + if codeHash != types.EmptyCodeHash { + code = rawdb.ReadCode(chaindb, codeHash) + if code == nil { + return common.Hash{}, fmt.Errorf("missing code for hash %x (account %x)", codeHash, addr) + } + stats.codes++ + } + + if err := binTrie.UpdateAccount(addr, &acc, len(code)); err != nil { + return common.Hash{}, fmt.Errorf("failed to update account %x: %w", addr, err) + } + if len(code) > 0 { + if err := binTrie.UpdateContractCode(addr, codeHash, code); err != nil { + return common.Hash{}, fmt.Errorf("failed to update code for %x: %w", addr, err) + } + } + + if acc.Root != types.EmptyRootHash { + addrHash := common.BytesToHash(accIter.Key) + storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acc.Root), srcTriedb) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to open storage trie for %x: %w", addr, err) + } + storageNodeIt, err := storageTrie.NodeIterator(nil) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to create storage iterator for %x: %w", addr, err) + } + storageIter := trie.NewIterator(storageNodeIt) + + slotCount := uint64(0) + for storageIter.Next() { + slotKey := storageTrie.GetKey(storageIter.Key) + if slotKey == nil { + return common.Hash{}, fmt.Errorf("missing preimage for storage key %x (account %x)", storageIter.Key, addr) + } + _, content, _, err := rlp.Split(storageIter.Value) + if err != nil { + return common.Hash{}, fmt.Errorf("invalid storage RLP for key %x (account %x): %w", slotKey, addr, err) + } + if err := binTrie.UpdateStorage(addr, slotKey, content); err != nil { + return common.Hash{}, fmt.Errorf("failed to update storage %x/%x: %w", addr, slotKey, err) + } + stats.slots++ + slotCount++ + + if slotCount%10000 == 0 { + binTrie, currentRoot, err = maybeCommit(binTrie, currentRoot, destTriedb, memLimit, stats) + if err != nil { + return common.Hash{}, err + } + } + } + if storageIter.Err != nil { + return common.Hash{}, fmt.Errorf("storage iteration error for %x: %w", addr, storageIter.Err) + } + } + stats.accounts++ + stats.report(false) + + if stats.accounts%1000 == 0 { + binTrie, currentRoot, err = maybeCommit(binTrie, currentRoot, destTriedb, memLimit, stats) + if err != nil { + return common.Hash{}, err + } + } + } + if accIter.Err != nil { + return common.Hash{}, fmt.Errorf("account iteration error: %w", accIter.Err) + } + + _, currentRoot, err = commitBinaryTrie(binTrie, currentRoot, destTriedb) + if err != nil { + return common.Hash{}, fmt.Errorf("final commit failed: %w", err) + } + stats.commits++ + stats.report(true) + return currentRoot, nil +} + +func maybeCommit(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *triedb.Database, memLimit uint64, stats *conversionStats) (*bintrie.BinaryTrie, common.Hash, error) { + if time.Since(stats.lastMemChk) < 5*time.Second { + return bt, currentRoot, nil + } + stats.lastMemChk = time.Now() + + var m runtime.MemStats + runtime.ReadMemStats(&m) + if m.Alloc < memLimit { + return bt, currentRoot, nil + } + log.Info("Memory limit reached, committing", "alloc", common.StorageSize(m.Alloc), "limit", common.StorageSize(memLimit)) + + bt, currentRoot, err := commitBinaryTrie(bt, currentRoot, destDB) + if err != nil { + return nil, common.Hash{}, err + } + stats.commits++ + stats.report(true) + return bt, currentRoot, nil +} + +func commitBinaryTrie(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *triedb.Database) (*bintrie.BinaryTrie, common.Hash, error) { + newRoot, nodeSet := bt.Commit(false) + if nodeSet != nil { + merged := trienode.NewWithNodeSet(nodeSet) + if err := destDB.Update(newRoot, currentRoot, 0, merged, triedb.NewStateSet()); err != nil { + return nil, common.Hash{}, fmt.Errorf("triedb update failed: %w", err) + } + if err := destDB.Commit(newRoot, false); err != nil { + return nil, common.Hash{}, fmt.Errorf("triedb commit failed: %w", err) + } + } + runtime.GC() + debug.FreeOSMemory() + + bt, err := bintrie.NewBinaryTrie(newRoot, destDB) + if err != nil { + return nil, common.Hash{}, fmt.Errorf("failed to reload binary trie: %w", err) + } + return bt, newRoot, nil +} + +func deleteMPTData(chaindb ethdb.Database, srcTriedb *triedb.Database, root common.Hash) error { + isPathDB := srcTriedb.Scheme() == rawdb.PathScheme + + srcTrie, err := trie.NewStateTrie(trie.StateTrieID(root), srcTriedb) + if err != nil { + return fmt.Errorf("failed to open source trie for deletion: %w", err) + } + acctIt, err := srcTrie.NodeIterator(nil) + if err != nil { + return fmt.Errorf("failed to create account iterator for deletion: %w", err) + } + batch := chaindb.NewBatch() + deleted := 0 + + for acctIt.Next(true) { + if isPathDB { + rawdb.DeleteAccountTrieNode(batch, acctIt.Path()) + } else { + node := acctIt.Hash() + if node != (common.Hash{}) { + rawdb.DeleteLegacyTrieNode(batch, node) + } + } + deleted++ + + if acctIt.Leaf() { + var acc types.StateAccount + if err := rlp.DecodeBytes(acctIt.LeafBlob(), &acc); err != nil { + return fmt.Errorf("invalid account during deletion: %w", err) + } + if acc.Root != types.EmptyRootHash { + addrHash := common.BytesToHash(acctIt.LeafKey()) + storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acc.Root), srcTriedb) + if err != nil { + return fmt.Errorf("failed to open storage trie for deletion: %w", err) + } + storageIt, err := storageTrie.NodeIterator(nil) + if err != nil { + return fmt.Errorf("failed to create storage iterator for deletion: %w", err) + } + for storageIt.Next(true) { + if isPathDB { + rawdb.DeleteStorageTrieNode(batch, addrHash, storageIt.Path()) + } else { + node := storageIt.Hash() + if node != (common.Hash{}) { + rawdb.DeleteLegacyTrieNode(batch, node) + } + } + deleted++ + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return fmt.Errorf("batch write failed: %w", err) + } + batch.Reset() + } + } + if storageIt.Error() != nil { + return fmt.Errorf("storage deletion iterator error: %w", storageIt.Error()) + } + } + } + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return fmt.Errorf("batch write failed: %w", err) + } + batch.Reset() + } + } + if acctIt.Error() != nil { + return fmt.Errorf("account deletion iterator error: %w", acctIt.Error()) + } + if batch.ValueSize() > 0 { + if err := batch.Write(); err != nil { + return fmt.Errorf("final batch write failed: %w", err) + } + } + log.Info("MPT deletion complete", "nodesDeleted", deleted) + return nil +} diff --git a/cmd/geth/bintrie_convert_test.go b/cmd/geth/bintrie_convert_test.go new file mode 100644 index 0000000000..9b95f6a70f --- /dev/null +++ b/cmd/geth/bintrie_convert_test.go @@ -0,0 +1,229 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie/bintrie" + "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/pathdb" + "github.com/holiman/uint256" +) + +func TestBintrieConvert(t *testing.T) { + var ( + addr1 = common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 = common.HexToAddress("0x2222222222222222222222222222222222222222") + slotKey1 = common.HexToHash("0x01") + slotKey2 = common.HexToHash("0x02") + slotVal1 = common.HexToHash("0xdeadbeef") + slotVal2 = common.HexToHash("0xcafebabe") + code = []byte{0x60, 0x42, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3} + ) + + chaindb := rawdb.NewMemoryDatabase() + + srcTriedb := triedb.NewDatabase(chaindb, &triedb.Config{ + Preimages: true, + PathDB: pathdb.Defaults, + }) + + gspec := &core.Genesis{ + Config: params.TestChainConfig, + BaseFee: big.NewInt(params.InitialBaseFee), + Alloc: types.GenesisAlloc{ + addr1: { + Balance: big.NewInt(1000000), + Nonce: 5, + }, + addr2: { + Balance: big.NewInt(2000000), + Nonce: 10, + Code: code, + Storage: map[common.Hash]common.Hash{ + slotKey1: slotVal1, + slotKey2: slotVal2, + }, + }, + }, + } + + genesisBlock := gspec.MustCommit(chaindb, srcTriedb) + root := genesisBlock.Root() + t.Logf("Genesis root: %x", root) + srcTriedb.Close() + + srcTriedb2 := triedb.NewDatabase(chaindb, &triedb.Config{ + Preimages: true, + PathDB: &pathdb.Config{ReadOnly: true}, + }) + defer srcTriedb2.Close() + + destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{ + IsVerkle: true, + PathDB: pathdb.Defaults, + }) + defer destTriedb.Close() + + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + if err != nil { + t.Fatalf("failed to create binary trie: %v", err) + } + + currentRoot, err := runConversionLoop(chaindb, srcTriedb2, destTriedb, bt, root, math.MaxUint64) + if err != nil { + t.Fatalf("conversion failed: %v", err) + } + t.Logf("Binary trie root: %x", currentRoot) + + bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb) + if err != nil { + t.Fatalf("failed to reload binary trie: %v", err) + } + + acc1, err := bt2.GetAccount(addr1) + if err != nil { + t.Fatalf("failed to get account1: %v", err) + } + if acc1 == nil { + t.Fatal("account1 not found in binary trie") + } + if acc1.Nonce != 5 { + t.Errorf("account1 nonce: got %d, want 5", acc1.Nonce) + } + wantBal1 := uint256.NewInt(1000000) + if acc1.Balance.Cmp(wantBal1) != 0 { + t.Errorf("account1 balance: got %s, want %s", acc1.Balance, wantBal1) + } + + acc2, err := bt2.GetAccount(addr2) + if err != nil { + t.Fatalf("failed to get account2: %v", err) + } + if acc2 == nil { + t.Fatal("account2 not found in binary trie") + } + if acc2.Nonce != 10 { + t.Errorf("account2 nonce: got %d, want 10", acc2.Nonce) + } + wantBal2 := uint256.NewInt(2000000) + if acc2.Balance.Cmp(wantBal2) != 0 { + t.Errorf("account2 balance: got %s, want %s", acc2.Balance, wantBal2) + } + + treeKey1 := bintrie.GetBinaryTreeKeyStorageSlot(addr2, slotKey1[:]) + val1, err := bt2.GetWithHashedKey(treeKey1) + if err != nil { + t.Fatalf("failed to get storage slot1: %v", err) + } + if len(val1) == 0 { + t.Fatal("storage slot1 not found") + } + got1 := common.BytesToHash(val1) + if got1 != slotVal1 { + t.Errorf("storage slot1: got %x, want %x", got1, slotVal1) + } + + treeKey2 := bintrie.GetBinaryTreeKeyStorageSlot(addr2, slotKey2[:]) + val2, err := bt2.GetWithHashedKey(treeKey2) + if err != nil { + t.Fatalf("failed to get storage slot2: %v", err) + } + if len(val2) == 0 { + t.Fatal("storage slot2 not found") + } + got2 := common.BytesToHash(val2) + if got2 != slotVal2 { + t.Errorf("storage slot2: got %x, want %x", got2, slotVal2) + } +} + +func TestBintrieConvertDeleteSource(t *testing.T) { + addr1 := common.HexToAddress("0x3333333333333333333333333333333333333333") + + chaindb := rawdb.NewMemoryDatabase() + + srcTriedb := triedb.NewDatabase(chaindb, &triedb.Config{ + Preimages: true, + PathDB: pathdb.Defaults, + }) + + gspec := &core.Genesis{ + Config: params.TestChainConfig, + BaseFee: big.NewInt(params.InitialBaseFee), + Alloc: types.GenesisAlloc{ + addr1: { + Balance: big.NewInt(1000000), + }, + }, + } + + genesisBlock := gspec.MustCommit(chaindb, srcTriedb) + root := genesisBlock.Root() + srcTriedb.Close() + + srcTriedb2 := triedb.NewDatabase(chaindb, &triedb.Config{ + Preimages: true, + PathDB: &pathdb.Config{ReadOnly: true}, + }) + + destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{ + IsVerkle: true, + PathDB: pathdb.Defaults, + }) + + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + if err != nil { + t.Fatalf("failed to create binary trie: %v", err) + } + + newRoot, err := runConversionLoop(chaindb, srcTriedb2, destTriedb, bt, root, math.MaxUint64) + if err != nil { + t.Fatalf("conversion failed: %v", err) + } + + if err := deleteMPTData(chaindb, srcTriedb2, root); err != nil { + t.Fatalf("deletion failed: %v", err) + } + srcTriedb2.Close() + + bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb) + if err != nil { + t.Fatalf("failed to reload binary trie after deletion: %v", err) + } + + acc, err := bt2.GetAccount(addr1) + if err != nil { + t.Fatalf("failed to get account after deletion: %v", err) + } + if acc == nil { + t.Fatal("account not found after MPT deletion") + } + wantBal := uint256.NewInt(1000000) + if acc.Balance.Cmp(wantBal) != 0 { + t.Errorf("balance after deletion: got %s, want %s", acc.Balance, wantBal) + } + destTriedb.Close() +} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index b72cbb9885..e196ac8688 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -260,6 +260,8 @@ func init() { utils.ShowDeprecated, // See snapshot.go snapshotCommand, + // See bintrie_convert.go + bintrieCommand, } if logTestCommand != nil { app.Commands = append(app.Commands, logTestCommand) From ea5448814f14a5e946522891e4d14399c1c888d3 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 10 Apr 2026 23:41:59 +0800 Subject: [PATCH 24/27] core/filtermaps: remove dead condition check (#34695) already check on line 40 before. --- core/filtermaps/math_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/filtermaps/math_test.go b/core/filtermaps/math_test.go index a4c1609059..0cd0046a7d 100644 --- a/core/filtermaps/math_test.go +++ b/core/filtermaps/math_test.go @@ -41,9 +41,7 @@ func TestSingleMatch(t *testing.T) { t.Fatalf("Invalid length of matches (got %d, expected 1)", len(matches)) } if matches[0] != lvIndex { - if len(matches) != 1 { - t.Fatalf("Incorrect match returned (got %d, expected %d)", matches[0], lvIndex) - } + t.Fatalf("Incorrect match returned (got %d, expected %d)", matches[0], lvIndex) } } } From f71a884e37857e6cbb093e2fc3e03b33bda85d6f Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:23:44 +0200 Subject: [PATCH 25/27] trie/bintrie: fix DeleteAccount no-op (#34676) `BinaryTrie.DeleteAccount` was a no-op, silently ignoring the caller's deletion request and leaving the old `BasicData` and `CodeHash` in the trie. Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- trie/bintrie/trie.go | 26 ++- trie/bintrie/trie_test.go | 328 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+), 5 deletions(-) diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 6c29239a87..45f9edd46c 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -216,10 +216,12 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error return nil, nil } - // If the account has been deleted, then values[10] will be 0 and not nil. If it has - // been recreated after that, then its code keccak will NOT be 0. So return `nil` if - // the nonce, and values[10], and code keccak is 0. - if bytes.Equal(values[BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[CodeHashLeafKey], zero[:]) { + // If the account has been deleted, BasicData and CodeHash will both be + // 32-byte zero blobs (not nil). If the account is recreated afterwards, + // UpdateAccount overwrites BasicData and CodeHash with non-zero values, + // so this branch won't activate.. + if bytes.Equal(values[BasicDataLeafKey], zero[:]) && + bytes.Equal(values[CodeHashLeafKey], zero[:]) { return nil, nil } @@ -294,8 +296,22 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er return nil } -// DeleteAccount is a no-op as it is disabled in stateless. +// DeleteAccount erases an account by overwriting the account +// descriptors with 0s. func (t *BinaryTrie) DeleteAccount(addr common.Address) error { + var ( + values = make([][]byte, StemNodeWidth) + stem = GetBinaryTreeKey(addr, zero[:]) + ) + // Clear BasicData (nonce, balance, code size) and CodeHash. + values[BasicDataLeafKey] = zero[:] + values[CodeHashLeafKey] = zero[:] + + root, err := t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0) + if err != nil { + return fmt.Errorf("DeleteAccount (%x) error: %v", addr, err) + } + t.root = root return nil } diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go index 256fd218e2..f420f53ef8 100644 --- a/trie/bintrie/trie_test.go +++ b/trie/bintrie/trie_test.go @@ -267,6 +267,334 @@ func TestStorageRoundTrip(t *testing.T) { } } +// newEmptyTestTrie creates a fresh BinaryTrie with an empty root and a +// default prevalue tracer. Use this for tests that populate the trie +// incrementally via Update*; for tests that want a pre-populated trie with +// a fixed entry set, use makeTrie (in iterator_test.go) instead. +func newEmptyTestTrie(t *testing.T) *BinaryTrie { + t.Helper() + return &BinaryTrie{ + root: NewBinaryNode(), + tracer: trie.NewPrevalueTracer(), + } +} + +// makeAccount constructs a StateAccount with the given fields. The Root is +// zeroed out because the bintrie has no per-account storage root. +func makeAccount(nonce uint64, balance uint64, codeHash common.Hash) *types.StateAccount { + return &types.StateAccount{ + Nonce: nonce, + Balance: uint256.NewInt(balance), + CodeHash: codeHash.Bytes(), + } +} + +// TestDeleteAccountRoundTrip verifies the basic delete path: create an +// account, read it back, delete it, confirm subsequent reads return nil. +// Regression test for the no-op DeleteAccount bug where the deletion was +// silently ignored and the old values remained in the trie. +func TestDeleteAccountRoundTrip(t *testing.T) { + tr := newEmptyTestTrie(t) + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + + // Create: write account, verify round-trip. + acc := makeAccount(42, 1000, codeHash) + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + got, err := tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if got == nil { + t.Fatal("GetAccount returned nil after UpdateAccount") + } + if got.Nonce != 42 { + t.Fatalf("Nonce: got %d, want 42", got.Nonce) + } + if got.Balance.Uint64() != 1000 { + t.Fatalf("Balance: got %s, want 1000", got.Balance) + } + if !bytes.Equal(got.CodeHash, codeHash[:]) { + t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash) + } + + // Delete: verify GetAccount returns nil afterwards. + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + got, err = tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount after delete: %v", err) + } + if got != nil { + t.Fatalf("GetAccount after delete: got %+v, want nil", got) + } +} + +// TestDeleteAccountOnMissingAccount verifies that deleting an account that +// was never created does not error and subsequent reads still return nil. +func TestDeleteAccountOnMissingAccount(t *testing.T) { + tr := newEmptyTestTrie(t) + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + + // Delete without any prior create. Should not panic or error on an + // empty root, and GetAccount should still return nil. + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount on empty trie: %v", err) + } + got, err := tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount after delete on empty trie: %v", err) + } + if got != nil { + t.Fatalf("GetAccount on deleted missing account: got %+v, want nil", got) + } +} + +// TestDeleteAccountPreservesOtherAccounts verifies that deleting one account +// does not affect accounts at different stems. +func TestDeleteAccountPreservesOtherAccounts(t *testing.T) { + tr := newEmptyTestTrie(t) + addrA := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + addrB := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + codeHashA := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + codeHashB := common.HexToHash("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0102030405060708090a0b0c0d0e0f10") + + // Create two distinct accounts. + if err := tr.UpdateAccount(addrA, makeAccount(1, 100, codeHashA), 0); err != nil { + t.Fatalf("UpdateAccount(A): %v", err) + } + if err := tr.UpdateAccount(addrB, makeAccount(2, 200, codeHashB), 0); err != nil { + t.Fatalf("UpdateAccount(B): %v", err) + } + + // Delete A. + if err := tr.DeleteAccount(addrA); err != nil { + t.Fatalf("DeleteAccount(A): %v", err) + } + + // A should be gone. + if got, err := tr.GetAccount(addrA); err != nil { + t.Fatalf("GetAccount(A): %v", err) + } else if got != nil { + t.Fatalf("GetAccount(A) after delete: got %+v, want nil", got) + } + + // B should still be readable with its original values. + got, err := tr.GetAccount(addrB) + if err != nil { + t.Fatalf("GetAccount(B): %v", err) + } + if got == nil { + t.Fatal("GetAccount(B) returned nil after unrelated delete") + } + if got.Nonce != 2 { + t.Fatalf("Account B Nonce: got %d, want 2", got.Nonce) + } + if got.Balance.Uint64() != 200 { + t.Fatalf("Account B Balance: got %s, want 200", got.Balance) + } + if !bytes.Equal(got.CodeHash, codeHashB[:]) { + t.Fatalf("Account B CodeHash: got %x, want %x", got.CodeHash, codeHashB) + } +} + +// TestDeleteAccountThenRecreate verifies that an account can be deleted and +// then recreated with different values; the second read must return the new +// values, not the stale ones from before deletion. +func TestDeleteAccountThenRecreate(t *testing.T) { + tr := newEmptyTestTrie(t) + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + codeHash1 := common.HexToHash("1111111111111111111111111111111111111111111111111111111111111111") + codeHash2 := common.HexToHash("2222222222222222222222222222222222222222222222222222222222222222") + + // Create. + if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash1), 0); err != nil { + t.Fatalf("UpdateAccount #1: %v", err) + } + // Delete. + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + // Recreate with new values. + if err := tr.UpdateAccount(addr, makeAccount(7, 9999, codeHash2), 0); err != nil { + t.Fatalf("UpdateAccount #2: %v", err) + } + // Read: must observe the new values, not the originals. + got, err := tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if got == nil { + t.Fatal("GetAccount returned nil after recreate") + } + if got.Nonce != 7 { + t.Fatalf("Nonce: got %d, want 7", got.Nonce) + } + if got.Balance.Uint64() != 9999 { + t.Fatalf("Balance: got %s, want 9999", got.Balance) + } + if !bytes.Equal(got.CodeHash, codeHash2[:]) { + t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash2) + } +} + +// TestDeleteAccountDoesNotAffectMainStorage verifies that DeleteAccount only +// clears the account's BasicData and CodeHash, leaving main storage slots +// untouched. Main storage slots live at different stems entirely (their +// keys route through the non-header branch in GetBinaryTreeKeyStorageSlot), +// so this test exercises the inter-stem isolation. Header-range storage +// slots share the same stem and are covered separately by +// TestDeleteAccountPreservesHeaderStorage. +// +// Wiping storage on self-destruct is a separate concern handled at the +// StateDB level. +func TestDeleteAccountDoesNotAffectMainStorage(t *testing.T) { + tr := newEmptyTestTrie(t) + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + + // Create account. + if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash), 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + // Write a main storage slot — i.e. key[31] >= 64 or key[:31] != 0 — so + // it lives at a different stem from the account header. + slot := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000080") + value := common.TrimLeftZeroes(common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef").Bytes()) + if err := tr.UpdateStorage(addr, slot[:], value); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + + // Delete the account. + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + + // Account should be absent. + got, err := tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount after delete: %v", err) + } + if got != nil { + t.Fatalf("GetAccount after delete: got %+v, want nil", got) + } + + // Main storage slot should still be readable — DeleteAccount must not + // have touched it. + stored, err := tr.GetStorage(addr, slot[:]) + if err != nil { + t.Fatalf("GetStorage after DeleteAccount: %v", err) + } + if len(stored) == 0 { + t.Fatal("main storage slot was wiped by DeleteAccount, expected it to survive") + } + var expected [HashSize]byte + copy(expected[HashSize-len(value):], value) + if !bytes.Equal(stored, expected[:]) { + t.Fatalf("main storage slot: got %x, want %x", stored, expected) + } +} + +// TestDeleteAccountPreservesHeaderStorage verifies that DeleteAccount does +// not clobber header-range storage slots (key[31] < 64), which live at the +// SAME stem as BasicData/CodeHash but at offsets 64-127. The safety here +// relies on StemNode.InsertValuesAtStem treating nil entries in the values +// slice as "do not overwrite"; this test pins that invariant so a future +// change cannot silently corrupt slots 0-63 of any contract. +func TestDeleteAccountPreservesHeaderStorage(t *testing.T) { + tr := newEmptyTestTrie(t) + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + + // Create account. + if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash), 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + + // Create a second, unrelated account so the root promotes from StemNode + // to InternalNode. BinaryTrie.GetStorage walks via root.Get, which is + // only implemented on InternalNode/Empty — calling it with a StemNode + // root panics. The existing main-storage test gets away with this because + // the main-storage slot lands on a separate stem and forces the same + // promotion implicitly; here we want a same-stem header slot, so the + // promotion has to come from a second account. + other := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + if err := tr.UpdateAccount(other, makeAccount(0, 0, common.Hash{}), 0); err != nil { + t.Fatalf("UpdateAccount(other): %v", err) + } + + // Write a header-range storage slot — key[:31] == 0 and key[31] < 64 + // — which routes through the header branch in GetBinaryTreeKeyStorageSlot + // and lands on the same stem as BasicData/CodeHash. + var slot [HashSize]byte + slot[31] = 5 + value := []byte{0xde, 0xad, 0xbe, 0xef} + if err := tr.UpdateStorage(addr, slot[:], value); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + + // Delete the account. + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + + // Account metadata should be gone. + got, err := tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount after delete: %v", err) + } + if got != nil { + t.Fatalf("GetAccount after delete: got %+v, want nil", got) + } + + // Header storage slot must survive — DeleteAccount only writes offsets + // BasicDataLeafKey, CodeHashLeafKey, and accountDeletedMarkerKey, leaving + // the header-storage offsets (64-127) untouched. + stored, err := tr.GetStorage(addr, slot[:]) + if err != nil { + t.Fatalf("GetStorage after DeleteAccount: %v", err) + } + if len(stored) == 0 { + t.Fatal("header storage slot was wiped by DeleteAccount, expected it to survive") + } + var expected [HashSize]byte + copy(expected[HashSize-len(value):], value) + if !bytes.Equal(stored, expected[:]) { + t.Fatalf("header storage slot: got %x, want %x", stored, expected) + } +} + +func TestDeleteAccountHashIsDeterministic(t *testing.T) { + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + acc := makeAccount(42, 1000, codeHash) + + run := func() common.Hash { + tr := newEmptyTestTrie(t) + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + return tr.Hash() + } + + first := run() + second := run() + if first != second { + t.Fatalf("non-deterministic root after Update+Delete: first=%x second=%x", first, second) + } + + empty := newEmptyTestTrie(t).Hash() + if first == empty { + t.Fatalf("post-delete root unexpectedly equals empty-trie root %x", empty) + } +} + func TestBinaryTrieWitness(t *testing.T) { tracer := trie.NewPrevalueTracer() From deda47f6a1c0588b9e2600ca97552994c2889d37 Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:43:48 +0200 Subject: [PATCH 26/27] =?UTF-8?q?trie/bintrie:=20fix=20GetAccount/GetStora?= =?UTF-8?q?ge=20non-membership=20=E2=80=94=20verify=20stem=20before=20retu?= =?UTF-8?q?rning=20values=20(#34690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix `GetAccount` returning **wrong account data** for non-existent addresses when the trie root is a `StemNode` (single-account trie) — the `StemNode` branch returned `r.Values` without verifying the queried address's stem matches. Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- trie/bintrie/stem_node.go | 5 +- trie/bintrie/stem_node_test.go | 44 +++++++++ trie/bintrie/trie.go | 2 +- trie/bintrie/trie_test.go | 159 +++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/trie/bintrie/stem_node.go b/trie/bintrie/stem_node.go index 3f69261d62..e5729e6182 100644 --- a/trie/bintrie/stem_node.go +++ b/trie/bintrie/stem_node.go @@ -37,7 +37,10 @@ type StemNode struct { // Get retrieves the value for the given key. func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) { - panic("this should not be called directly") + if !bytes.Equal(bt.Stem, key[:StemSize]) { + return nil, nil + } + return bt.Values[key[StemSize]], nil } // Insert inserts a new key-value pair into the node. diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go index 92c1b49e02..310c553d39 100644 --- a/trie/bintrie/stem_node_test.go +++ b/trie/bintrie/stem_node_test.go @@ -23,6 +23,50 @@ import ( "github.com/ethereum/go-ethereum/common" ) +// TestStemNodeGet tests the Get method for matching stem, non-matching stem, +// and nil-value suffix scenarios. +func TestStemNodeGet(t *testing.T) { + stem := make([]byte, StemSize) + stem[0] = 0xAB + var values [StemNodeWidth][]byte + values[5] = common.HexToHash("0xdeadbeef").Bytes() + + node := &StemNode{Stem: stem, Values: values[:], depth: 0} + + // Matching stem, populated suffix → returns value. + key := make([]byte, HashSize) + copy(key[:StemSize], stem) + key[StemSize] = 5 + got, err := node.Get(key, nil) + if err != nil { + t.Fatalf("Get error: %v", err) + } + if !bytes.Equal(got, values[5]) { + t.Fatalf("Get = %x, want %x", got, values[5]) + } + + // Matching stem, empty suffix → returns nil (slot not set). + key[StemSize] = 99 + got, err = node.Get(key, nil) + if err != nil { + t.Fatalf("Get error: %v", err) + } + if got != nil { + t.Fatalf("Get(empty suffix) = %x, want nil", got) + } + + // Non-matching stem → returns nil, nil. + otherKey := make([]byte, HashSize) + otherKey[0] = 0xFF + got, err = node.Get(otherKey, nil) + if err != nil { + t.Fatalf("Get error: %v", err) + } + if got != nil { + t.Fatalf("Get(wrong stem) = %x, want nil", got) + } +} + // TestStemNodeInsertSameStem tests inserting values with the same stem func TestStemNodeInsertSameStem(t *testing.T) { stem := make([]byte, 31) diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 45f9edd46c..b1e3c991c0 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -191,7 +191,7 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error case *InternalNode: values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver) case *StemNode: - values = r.Values + values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver) case Empty: return nil, nil default: diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go index f420f53ef8..5b104ddde4 100644 --- a/trie/bintrie/trie_test.go +++ b/trie/bintrie/trie_test.go @@ -620,3 +620,162 @@ func TestBinaryTrieWitness(t *testing.T) { t.Fatal("unexpected witness value for path2") } } + +// testAccount is a helper that creates a BinaryTrie with a tracer and +// inserts a single account, returning the trie. +func testAccount(t *testing.T, addr common.Address, nonce uint64, balance uint64) *BinaryTrie { + t.Helper() + tr := &BinaryTrie{ + root: NewBinaryNode(), + tracer: trie.NewPrevalueTracer(), + } + acc := &types.StateAccount{ + Nonce: nonce, + Balance: uint256.NewInt(balance), + CodeHash: types.EmptyCodeHash[:], + } + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount error: %v", err) + } + return tr +} + +// TestGetAccountNonMembershipStemRoot verifies that querying a non-existent +// address returns nil when the trie root is a StemNode (single-account trie). +// This is a regression test: previously the StemNode branch in GetAccount +// returned the root's values without verifying the stem. +func TestGetAccountNonMembershipStemRoot(t *testing.T) { + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + tr := testAccount(t, addr, 42, 100) + + // Verify root is a StemNode (single stem inserted). + if _, ok := tr.root.(*StemNode); !ok { + t.Fatalf("expected StemNode root, got %T", tr.root) + } + + // Query a completely different address — must return nil. + other := common.HexToAddress("0x2222222222222222222222222222222222222222") + got, err := tr.GetAccount(other) + if err != nil { + t.Fatalf("GetAccount error: %v", err) + } + if got != nil { + t.Fatalf("expected nil for non-existent account, got nonce=%d balance=%s", got.Nonce, got.Balance) + } + + // Original account must still be retrievable. + got, err = tr.GetAccount(addr) + if err != nil { + t.Fatalf("GetAccount(original) error: %v", err) + } + if got == nil { + t.Fatal("expected original account, got nil") + } + if got.Nonce != 42 { + t.Fatalf("expected nonce=42, got %d", got.Nonce) + } +} + +// TestGetAccountNonMembershipInternalRoot verifies that querying a non-existent +// address returns nil when the trie root is an InternalNode (multi-account trie). +func TestGetAccountNonMembershipInternalRoot(t *testing.T) { + tr := &BinaryTrie{ + root: NewBinaryNode(), + tracer: trie.NewPrevalueTracer(), + } + + // Insert two accounts whose binary tree keys have different first bits + // so the root splits into an InternalNode. + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x9999999999999999999999999999999999999999") + for _, addr := range []common.Address{addr1, addr2} { + acc := &types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1), + CodeHash: types.EmptyCodeHash[:], + } + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount error: %v", err) + } + } + + // Verify root is an InternalNode. + if _, ok := tr.root.(*InternalNode); !ok { + t.Fatalf("expected InternalNode root, got %T", tr.root) + } + + // Query a non-existent address — must return nil. + other := common.HexToAddress("0x5555555555555555555555555555555555555555") + got, err := tr.GetAccount(other) + if err != nil { + t.Fatalf("GetAccount error: %v", err) + } + if got != nil { + t.Fatalf("expected nil for non-existent account, got nonce=%d", got.Nonce) + } +} + +// TestGetStorageNonMembershipStemRoot verifies that querying storage for a +// non-existent address returns nil when the root is a StemNode. This is a +// regression test: previously StemNode.Get panicked unconditionally. +func TestGetStorageNonMembershipStemRoot(t *testing.T) { + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + tr := testAccount(t, addr, 1, 100) + + // Verify root is a StemNode. + if _, ok := tr.root.(*StemNode); !ok { + t.Fatalf("expected StemNode root, got %T", tr.root) + } + + // Query storage for a different address — must return nil, not panic. + other := common.HexToAddress("0x2222222222222222222222222222222222222222") + slot := common.HexToHash("0x01") + got, err := tr.GetStorage(other, slot[:]) + if err != nil { + t.Fatalf("GetStorage error: %v", err) + } + if len(got) > 0 && !bytes.Equal(got, zero[:]) { + t.Fatalf("expected nil/zero for non-existent storage, got %x", got) + } +} + +// TestGetStorageNonMembershipInternalRoot verifies that querying storage for a +// non-existent address returns nil when the root is an InternalNode. +func TestGetStorageNonMembershipInternalRoot(t *testing.T) { + tr := &BinaryTrie{ + root: NewBinaryNode(), + tracer: trie.NewPrevalueTracer(), + } + + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + acc := &types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1000), + CodeHash: types.EmptyCodeHash[:], + } + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount error: %v", err) + } + + // Add a storage slot so the root becomes an InternalNode (storage + // slots use a different stem than account data). + slot := common.HexToHash("0xFF") + val := common.TrimLeftZeroes(common.HexToHash("0xdeadbeef").Bytes()) + if err := tr.UpdateStorage(addr, slot[:], val); err != nil { + t.Fatalf("UpdateStorage error: %v", err) + } + + if _, ok := tr.root.(*InternalNode); !ok { + t.Fatalf("expected InternalNode root, got %T", tr.root) + } + + // Query storage for a non-existent address — must return nil. + other := common.HexToAddress("0x9999999999999999999999999999999999999999") + got, err := tr.GetStorage(other, slot[:]) + if err != nil { + t.Fatalf("GetStorage error: %v", err) + } + if len(got) > 0 && !bytes.Equal(got, zero[:]) { + t.Fatalf("expected nil/zero for non-existent storage, got %x", got) + } +} From 6333855163d0ac99240f966df875be64c0885983 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 13 Apr 2026 08:09:42 +0200 Subject: [PATCH 27/27] core: turn gas into a vector (#34691) Pre-refactor PR to get 8037 upstreamed in chunks --------- Co-authored-by: Gary Rong --- core/vm/contract.go | 14 +-- core/vm/eips.go | 6 +- core/vm/evm.go | 14 +-- core/vm/gas_table.go | 190 ++++++++++++++++++---------------- core/vm/gascosts.go | 36 +++++++ core/vm/instructions.go | 6 +- core/vm/instructions_test.go | 2 +- core/vm/interpreter.go | 16 +-- core/vm/jump_table.go | 2 +- core/vm/operations_acl.go | 98 +++++++++--------- core/vm/operations_verkle.go | 115 ++++++++++---------- eth/tracers/js/tracer_test.go | 4 +- 12 files changed, 278 insertions(+), 225 deletions(-) create mode 100644 core/vm/gascosts.go diff --git a/core/vm/contract.go b/core/vm/contract.go index 165ca833f8..3b5695e21a 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -42,7 +42,7 @@ type Contract struct { IsDeployment bool IsSystemCall bool - Gas uint64 + Gas GasCosts value *uint256.Int } @@ -56,7 +56,7 @@ func NewContract(caller common.Address, address common.Address, value *uint256.I caller: caller, address: address, jumpDests: jumpDests, - Gas: gas, + Gas: GasCosts{RegularGas: gas}, value: value, } } @@ -127,13 +127,13 @@ func (c *Contract) Caller() common.Address { // UseGas attempts the use gas and subtracts it and returns true on success func (c *Contract) UseGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) { - if c.Gas < gas { + if c.Gas.RegularGas < gas { return false } if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(c.Gas, c.Gas-gas, reason) + logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas-gas, reason) } - c.Gas -= gas + c.Gas.RegularGas -= gas return true } @@ -143,9 +143,9 @@ func (c *Contract) RefundGas(gas uint64, logger *tracing.Hooks, reason tracing.G return } if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(c.Gas, c.Gas+gas, reason) + logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas+gas, reason) } - c.Gas += gas + c.Gas.RegularGas += gas } // Address returns the contracts address diff --git a/core/vm/eips.go b/core/vm/eips.go index 3ccd9aaaf0..8f4ca3ae41 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -381,7 +381,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er addr := common.Address(a.Bytes20()) code := evm.StateDB.GetCode(addr) paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) - consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas) + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas) scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas @@ -407,7 +407,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // touch next chunk if PUSH1 is at the boundary. if so, *pc has // advanced past this boundary. contractAddr := scope.Contract.Address() - consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) scope.Contract.UseGas(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas @@ -435,7 +435,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { contractAddr := scope.Contract.Address() - consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas diff --git a/core/vm/evm.go b/core/vm/evm.go index 36494de2a8..4df2627486 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -303,7 +303,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g contract.IsSystemCall = isSystemCall(caller) contract.SetCallCode(evm.resolveCodeHash(addr), code) ret, err = evm.Run(contract, input, false) - gas = contract.Gas + gas = contract.Gas.RegularGas } } // When an error was returned by the EVM or when setting the creation code @@ -365,7 +365,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt contract := NewContract(caller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) - gas = contract.Gas + gas = contract.Gas.RegularGas } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -413,7 +413,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) - gas = contract.Gas + gas = contract.Gas.RegularGas } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -472,7 +472,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // above we revert to the snapshot and consume any gas remaining. Additionally // when we're in Homestead this also counts for code storage gas errors. ret, err = evm.Run(contract, input, true) - gas = contract.Gas + gas = contract.Gas.RegularGas } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -583,10 +583,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) + contract.UseGas(contract.Gas.RegularGas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } } - return ret, address, contract.Gas, err + return ret, address, contract.Gas.RegularGas, err } // initNewContract runs a new contract's creation code, performs checks on the @@ -613,7 +613,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b return ret, ErrCodeStoreOutOfGas } } else { - consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas) + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if len(ret) > 0 && (consumed < wanted) { return ret, ErrCodeStoreOutOfGas diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index f075a99468..b3259b2ec7 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -64,26 +64,26 @@ func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) { // EXTCODECOPY (stack position 3) // RETURNDATACOPY (stack position 2) func memoryCopierGas(stackpos int) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { // Gas for expanding the memory gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } // And gas for copying data, charged per word at param.CopyGas words, overflow := stack.Back(stackpos).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if words, overflow = math.SafeMul(toWordSize(words), params.CopyGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if gas, overflow = math.SafeAdd(gas, words); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } } @@ -95,9 +95,9 @@ var ( gasReturnDataCopy = memoryCopierGas(2) ) -func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } var ( y, x = stack.Back(1), stack.Back(0) @@ -114,12 +114,12 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi // 3. From a non-zero to a non-zero (CHANGE) switch { case current == (common.Hash{}) && y.Sign() != 0: // 0 => non 0 - return params.SstoreSetGas, nil + return GasCosts{RegularGas: params.SstoreSetGas}, nil case current != (common.Hash{}) && y.Sign() == 0: // non 0 => 0 evm.StateDB.AddRefund(params.SstoreRefundGas) - return params.SstoreClearGas, nil + return GasCosts{RegularGas: params.SstoreClearGas}, nil default: // non 0 => non 0 (or 0 => 0) - return params.SstoreResetGas, nil + return GasCosts{RegularGas: params.SstoreResetGas}, nil } } @@ -139,16 +139,16 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi // (2.2.2.2.) Otherwise, add 4800 gas to refund counter. value := common.Hash(y.Bytes32()) if current == value { // noop (1) - return params.NetSstoreNoopGas, nil + return GasCosts{RegularGas: params.NetSstoreNoopGas}, nil } if original == current { if original == (common.Hash{}) { // create slot (2.1.1) - return params.NetSstoreInitGas, nil + return GasCosts{RegularGas: params.NetSstoreInitGas}, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) evm.StateDB.AddRefund(params.NetSstoreClearRefund) } - return params.NetSstoreCleanGas, nil // write existing slot (2.1.2) + return GasCosts{RegularGas: params.NetSstoreCleanGas}, nil // write existing slot (2.1.2) } if original != (common.Hash{}) { if current == (common.Hash{}) { // recreate slot (2.2.1.1) @@ -164,7 +164,7 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi evm.StateDB.AddRefund(params.NetSstoreResetRefund) } } - return params.NetSstoreDirtyGas, nil + return GasCosts{RegularGas: params.NetSstoreDirtyGas}, nil } // Here come the EIP2200 rules: @@ -182,13 +182,13 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi // (2.2.2.) If original value equals new value (this storage slot is reset): // (2.2.2.1.) If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. // (2.2.2.2.) Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. -func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } // If we fail the minimum gas availability invariant, fail (0) - if contract.Gas <= params.SstoreSentryGasEIP2200 { - return 0, errors.New("not enough gas for reentrancy sentry") + if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { + return GasCosts{}, errors.New("not enough gas for reentrancy sentry") } // Gas sentry honoured, do the actual gas calculation based on the stored value var ( @@ -198,16 +198,16 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m value := common.Hash(y.Bytes32()) if current == value { // noop (1) - return params.SloadGasEIP2200, nil + return GasCosts{RegularGas: params.SloadGasEIP2200}, nil } if original == current { if original == (common.Hash{}) { // create slot (2.1.1) - return params.SstoreSetGasEIP2200, nil + return GasCosts{RegularGas: params.SstoreSetGasEIP2200}, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP2200) } - return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + return GasCosts{RegularGas: params.SstoreResetGasEIP2200}, nil // write existing slot (2.1.2) } if original != (common.Hash{}) { if current == (common.Hash{}) { // recreate slot (2.2.1.1) @@ -223,62 +223,66 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) } } - return params.SloadGasEIP2200, nil // dirty update (2.2) + return GasCosts{RegularGas: params.SloadGasEIP2200}, nil // dirty update (2.2) } func makeGasLog(n uint64) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { requestedSize, overflow := stack.Back(1).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } if gas, overflow = math.SafeAdd(gas, params.LogGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if gas, overflow = math.SafeAdd(gas, n*params.LogTopicGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } var memorySizeGas uint64 if memorySizeGas, overflow = math.SafeMul(requestedSize, params.LogDataGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if gas, overflow = math.SafeAdd(gas, memorySizeGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } } -func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } wordGas, overflow := stack.Back(1).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if wordGas, overflow = math.SafeMul(toWordSize(wordGas), params.Keccak256WordGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if gas, overflow = math.SafeAdd(gas, wordGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } // pureMemoryGascost is used by several operations, which aside from their // static cost have a dynamic cost which is solely based on the memory // expansion -func pureMemoryGascost(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return memoryGasCost(mem, memorySize) +func pureMemoryGascost(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + return GasCosts{RegularGas: gas}, nil } var ( @@ -290,64 +294,64 @@ var ( gasCreate = pureMemoryGascost ) -func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } wordGas, overflow := stack.Back(2).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if wordGas, overflow = math.SafeMul(toWordSize(wordGas), params.Keccak256WordGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if gas, overflow = math.SafeAdd(gas, wordGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } size, overflow := stack.Back(2).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { - return 0, err + return GasCosts{}, err } // Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow moreGas := params.InitCodeWordGas * ((size + 31) / 32) if gas, overflow = math.SafeAdd(gas, moreGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } size, overflow := stack.Back(2).Uint64WithOverflow() if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { - return 0, err + return GasCosts{}, err } // Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow moreGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32) if gas, overflow = math.SafeAdd(gas, moreGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) var ( @@ -355,12 +359,12 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem overflow bool ) if gas, overflow = math.SafeAdd(gas, params.ExpGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) var ( @@ -368,9 +372,9 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor overflow bool ) if gas, overflow = math.SafeAdd(gas, params.ExpGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } var ( @@ -381,36 +385,36 @@ var ( ) func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsic, stack.Back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.Back(0)) if err != nil { - return 0, err + return GasCosts{}, err } - gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp) + gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp) if overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } } -func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( gas uint64 transfersValue = !stack.Back(2).IsZero() address = common.Address(stack.Back(1).Bytes20()) ) if evm.readOnly && transfersValue { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } // Stateless check memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } var transferGas uint64 if transfersValue && !evm.chainRules.IsEIP4762 { @@ -418,12 +422,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } var overflow bool if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. - if contract.Gas < gas { - return 0, ErrOutOfGas + if contract.Gas.RegularGas < gas { + return GasCosts{}, ErrOutOfGas } // Stateful check var stateGas uint64 @@ -435,15 +439,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m stateGas += params.CallNewAccountGas } if gas, overflow = math.SafeAdd(gas, stateGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } var ( gas uint64 @@ -453,22 +457,30 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor gas += params.CallValueTransferGas } if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return memoryGasCost(mem, memorySize) +func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + return GasCosts{RegularGas: gas}, nil } -func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return memoryGasCost(mem, memorySize) +func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + return GasCosts{RegularGas: gas}, nil } -func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } var gas uint64 @@ -490,5 +502,5 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me if !evm.StateDB.HasSelfDestructed(contract.Address()) { evm.StateDB.AddRefund(params.SelfdestructRefundGas) } - return gas, nil + return GasCosts{RegularGas: gas}, nil } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go new file mode 100644 index 0000000000..ba6746758b --- /dev/null +++ b/core/vm/gascosts.go @@ -0,0 +1,36 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import "fmt" + +// GasCosts denotes a vector of gas costs in the +// multidimensional metering paradigm. +type GasCosts struct { + RegularGas uint64 + StateGas uint64 +} + +// Sum returns the total gas (regular + state). +func (g GasCosts) Sum() uint64 { + return g.RegularGas + g.StateGas +} + +// String returns a visual representation of the gas vector. +func (g GasCosts) String() string { + return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index a5fa11e307..74400732ac 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -566,7 +566,7 @@ func opMsize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opGas(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas)) + scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas.RegularGas)) return nil, nil } @@ -658,7 +658,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { value = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas + gas = scope.Contract.Gas.RegularGas ) if evm.chainRules.IsEIP150 { gas -= gas / 64 @@ -702,7 +702,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { offset, size = scope.Stack.pop(), scope.Stack.pop() salt = scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas + gas = scope.Contract.Gas.RegularGas ) // Apply EIP150 diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index 4c6d093d2e..1f69eea3da 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -879,7 +879,7 @@ func TestOpMCopy(t *testing.T) { if dynamicCost, err := gasMcopy(evm, nil, stack, mem, memorySize); err != nil { t.Error(err) } else { - haveGas = GasFastestStep + dynamicCost + haveGas = GasFastestStep + dynamicCost.RegularGas } // Expand mem if memorySize > 0 { diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 620c069fc8..b507595fab 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -166,14 +166,14 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte for { if debug { // Capture pre-execution values for tracing. - logged, pcCopy, gasCopy = false, pc, contract.Gas + logged, pcCopy, gasCopy = false, pc, contract.Gas.RegularGas } if isEIP4762 && !contract.IsDeployment && !contract.IsSystemCall { // if the PC ends up in a new "chunk" of verkleized code, charge the // associated costs. contractAddr := contract.Address() - consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas) + consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas) contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if consumed < wanted { return nil, ErrOutOfGas @@ -192,10 +192,10 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack} } // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas < cost { + if contract.Gas.RegularGas < cost { return nil, ErrOutOfGas } else { - contract.Gas -= cost + contract.Gas.RegularGas -= cost } // All ops with a dynamic memory usage also has a dynamic gas cost. @@ -218,17 +218,17 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte } // Consume the gas and return an error if not enough gas is available. // cost is explicitly set so that the capture state defer method can get the proper cost - var dynamicCost uint64 + var dynamicCost GasCosts dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize) - cost += dynamicCost // for tracing + cost += dynamicCost.RegularGas // for tracing if err != nil { return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) } // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas < dynamicCost { + if contract.Gas.RegularGas < dynamicCost.RegularGas { return nil, ErrOutOfGas } else { - contract.Gas -= dynamicCost + contract.Gas.RegularGas -= dynamicCost.RegularGas } } diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index a2e2c91194..82fc43ec13 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -24,7 +24,7 @@ import ( type ( executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) - gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64 + gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64 // memorySizeFunc returns the required size, and whether the operation overflowed a uint64 memorySizeFunc func(*Stack) (size uint64, overflow bool) ) diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index addd2b162f..154c261cae 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -27,13 +27,13 @@ import ( ) func makeGasSStoreFunc(clearingRefund uint64) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } // If we fail the minimum gas availability invariant, fail (0) - if contract.Gas <= params.SstoreSentryGasEIP2200 { - return 0, errors.New("not enough gas for reentrancy sentry") + if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { + return GasCosts{}, errors.New("not enough gas for reentrancy sentry") } // Gas sentry honoured, do the actual gas calculation based on the stored value var ( @@ -53,18 +53,18 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil - return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS + return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS } if original == current { if original == (common.Hash{}) { // create slot (2.1.1) - return cost + params.SstoreSetGasEIP2200, nil + return GasCosts{RegularGas: cost + params.SstoreSetGasEIP2200}, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) evm.StateDB.AddRefund(clearingRefund) } // EIP-2200 original clause: // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) - return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2) + return GasCosts{RegularGas: cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929)}, nil // write existing slot (2.1.2) } if original != (common.Hash{}) { if current == (common.Hash{}) { // recreate slot (2.2.1.1) @@ -89,7 +89,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } // EIP-2200 original clause: //return params.SloadGasEIP2200, nil // dirty update (2.2) - return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2) + return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2) } } @@ -98,7 +98,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { // whose storage is being read) is not yet in accessed_storage_keys, // charge 2100 gas and add the pair to accessed_storage_keys. // If the pair is already in accessed_storage_keys, charge 100 gas. -func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { loc := stack.peek() slot := common.Hash(loc.Bytes32()) // Check slot presence in the access list @@ -106,9 +106,9 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me // If the caller cannot afford the cost, this change will be rolled back // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) - return params.ColdSloadCostEIP2929, nil + return GasCosts{RegularGas: params.ColdSloadCostEIP2929}, nil } - return params.WarmStorageReadCostEIP2929, nil + return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil } // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 @@ -116,12 +116,13 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me // > If the target is not in accessed_addresses, // > charge COLD_ACCOUNT_ACCESS_COST gas, and add the address to accessed_addresses. // > Otherwise, charge WARM_STORAGE_READ_COST gas. -func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { // memory expansion first (dynamic part of pre-2929 implementation) - gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) + gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } + gas := gasCost.RegularGas addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { @@ -129,11 +130,11 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo var overflow bool // We charge (cold-warm), since 'warm' is already charged as constantGas if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } - return gas, nil + return GasCosts{RegularGas: gas}, nil } // gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list. @@ -143,20 +144,20 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo // - extcodehash, // - extcodesize, // - (ext) balance -func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(addr) // The warm storage read cost is already charged as constantGas - return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil + return GasCosts{RegularGas: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929}, nil } - return 0, nil + return GasCosts{}, nil } func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { addr := common.Address(stack.Back(addressPosition).Bytes20()) // Check slot presence in the access list warmAccess := evm.StateDB.AddressInAccessList(addr) @@ -168,7 +169,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // Charge the remaining difference here already, to correctly calculate available // gas for call if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { - return 0, ErrOutOfGas + return GasCosts{}, ErrOutOfGas } } // Now call the old calculator, which takes into account @@ -176,21 +177,22 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // - transfer value // - memory expansion // - 63/64ths rule - gas, err := oldCalculator(evm, contract, stack, mem, memorySize) + gasCost, err := oldCalculator(evm, contract, stack, mem, memorySize) if warmAccess || err != nil { - return gas, err + return gasCost, err } // In case of a cold access, we temporarily add the cold charge back, and also // add it to the returned gas. By adding it to the return, it will be charged // outside of this function, as part of the dynamic gas, and that will make it // also become correctly reported to tracers. - contract.Gas += coldCost + contract.Gas.RegularGas += coldCost + gas := gasCost.RegularGas var overflow bool if gas, overflow = math.SafeAdd(gas, coldCost); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } } @@ -224,13 +226,13 @@ var ( // makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-3529 func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { - gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( gas uint64 address = common.Address(stack.peek().Bytes20()) ) if evm.readOnly { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } if !evm.StateDB.AddressInAccessList(address) { // If the caller cannot afford the cost, this change will be rolled back @@ -239,8 +241,8 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps - if contract.Gas < gas { - return 0, ErrOutOfGas + if contract.Gas.RegularGas < gas { + return GasCosts{}, ErrOutOfGas } } // if empty and transfers value @@ -250,7 +252,7 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { if refundsEnabled && !evm.StateDB.HasSelfDestructed(contract.Address()) { evm.StateDB.AddRefund(params.SelfdestructRefundGas) } - return gas, nil + return GasCosts{RegularGas: gas}, nil } return gasFunc } @@ -262,20 +264,20 @@ var ( gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic) ) -func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { // Return early if this call attempts to transfer value in a static context. // Although it's checked in `gasCall`, EIP-7702 loads the target's code before // to determine if it is resolving a delegation. This could incorrectly record // the target in the block access list (BAL) if the call later fails. transfersValue := !stack.Back(2).IsZero() if evm.readOnly && transfersValue { - return 0, ErrWriteProtection + return GasCosts{}, ErrWriteProtection } return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) } func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 eip7702Cost uint64 @@ -294,7 +296,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // Charge the remaining difference here already, to correctly calculate // available gas for call if !contract.UseGas(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { - return 0, ErrOutOfGas + return GasCosts{}, ErrOutOfGas } } @@ -305,13 +307,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // - create new account intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. // It's an essential safeguard before any stateful check. - if contract.Gas < intrinsicCost { - return 0, ErrOutOfGas + if contract.Gas.RegularGas < intrinsicCost.RegularGas { + return GasCosts{}, ErrOutOfGas } // Check if code is a delegation and if so, charge for resolution. @@ -323,20 +325,20 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { eip7702Cost = params.ColdAccountAccessCostEIP2929 } if !contract.UseGas(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { - return 0, ErrOutOfGas + return GasCosts{}, ErrOutOfGas } } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsicCost, stack.Back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.Back(0)) if err != nil { - return 0, err + return GasCosts{}, err } // Temporarily add the gas charge back to the contract and return value. By // adding it to the return, it will be charged outside of this function, as // part of the dynamic gas. This will ensure it is correctly reported to // tracers. - contract.Gas += eip2929Cost + eip7702Cost + contract.Gas.RegularGas += eip2929Cost + eip7702Cost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls. @@ -345,14 +347,14 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { totalCost uint64 ) if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow { - return 0, ErrGasUintOverflow + if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow { + return GasCosts{}, ErrGasUintOverflow } if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return totalCost, nil + return GasCosts{RegularGas: totalCost}, nil } } diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index 30f9957775..d57f2c4dcf 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -24,37 +24,37 @@ import ( "github.com/ethereum/go-ethereum/params" ) -func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true, contract.Gas, true), nil +func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + return GasCosts{RegularGas: evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true, contract.Gas.RegularGas, true)}, nil } -func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, contract.Gas, true), nil +func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + return GasCosts{RegularGas: evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, contract.Gas.RegularGas, true)}, nil } -func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { address := stack.peek().Bytes20() - return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil + return GasCosts{RegularGas: evm.AccessEvents.BasicDataGas(address, false, contract.Gas.RegularGas, true)}, nil } -func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { address := stack.peek().Bytes20() if _, isPrecompile := evm.precompile(address); isPrecompile { - return 0, nil + return GasCosts{}, nil } - return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil + return GasCosts{RegularGas: evm.AccessEvents.BasicDataGas(address, false, contract.Gas.RegularGas, true)}, nil } -func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { address := stack.peek().Bytes20() if _, isPrecompile := evm.precompile(address); isPrecompile { - return 0, nil + return GasCosts{}, nil } - return evm.AccessEvents.CodeHashGas(address, false, contract.Gas, true), nil + return GasCosts{RegularGas: evm.AccessEvents.CodeHashGas(address, false, contract.Gas.RegularGas, true)}, nil } func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc { - return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( target = common.Address(stack.Back(1).Bytes20()) witnessGas uint64 @@ -65,9 +65,9 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga // If value is transferred, it is charged before 1/64th // is subtracted from the available gas pool. if withTransferCosts && !stack.Back(2).IsZero() { - wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas) - if wantedValueTransferWitnessGas > contract.Gas { - return wantedValueTransferWitnessGas, nil + wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas.RegularGas) + if wantedValueTransferWitnessGas > contract.Gas.RegularGas { + return GasCosts{RegularGas: wantedValueTransferWitnessGas}, nil } witnessGas = wantedValueTransferWitnessGas } else if isPrecompile || isSystemContract { @@ -78,25 +78,26 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga // (so before we get to this point) // But the message call is part of the subcall, for which only 63/64th // of the gas should be available. - wantedMessageCallWitnessGas := evm.AccessEvents.MessageCallGas(target, contract.Gas-witnessGas) + wantedMessageCallWitnessGas := evm.AccessEvents.MessageCallGas(target, contract.Gas.RegularGas-witnessGas) var overflow bool if witnessGas, overflow = math.SafeAdd(witnessGas, wantedMessageCallWitnessGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - if witnessGas > contract.Gas { - return witnessGas, nil + if witnessGas > contract.Gas.RegularGas { + return GasCosts{RegularGas: witnessGas}, nil } } - contract.Gas -= witnessGas + contract.Gas.RegularGas -= witnessGas // if the operation fails, adds witness gas to the gas before returning the error - gas, err := oldCalculator(evm, contract, stack, mem, memorySize) - contract.Gas += witnessGas // restore witness gas so that it can be charged at the callsite + gasCost, err := oldCalculator(evm, contract, stack, mem, memorySize) + contract.Gas.RegularGas += witnessGas // restore witness gas so that it can be charged at the callsite + gas := gasCost.RegularGas var overflow bool if gas, overflow = math.SafeAdd(gas, witnessGas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, err + return GasCosts{RegularGas: gas}, err } } @@ -107,18 +108,18 @@ var ( gasDelegateCallEIP4762 = makeCallVariantGasEIP4762(gasDelegateCall, false) ) -func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { beneficiaryAddr := common.Address(stack.peek().Bytes20()) if _, isPrecompile := evm.precompile(beneficiaryAddr); isPrecompile { - return 0, nil + return GasCosts{}, nil } if contract.IsSystemCall { - return 0, nil + return GasCosts{}, nil } contractAddr := contract.Address() - wanted := evm.AccessEvents.BasicDataGas(contractAddr, false, contract.Gas, false) - if wanted > contract.Gas { - return wanted, nil + wanted := evm.AccessEvents.BasicDataGas(contractAddr, false, contract.Gas.RegularGas, false) + if wanted > contract.Gas.RegularGas { + return GasCosts{RegularGas: wanted}, nil } statelessGas := wanted balanceIsZero := evm.StateDB.GetBalance(contractAddr).Sign() == 0 @@ -126,44 +127,45 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem isSystemContract := beneficiaryAddr == params.HistoryStorageAddress if (isPrecompile || isSystemContract) && balanceIsZero { - return statelessGas, nil + return GasCosts{RegularGas: statelessGas}, nil } if contractAddr != beneficiaryAddr { - wanted := evm.AccessEvents.BasicDataGas(beneficiaryAddr, false, contract.Gas-statelessGas, false) - if wanted > contract.Gas-statelessGas { - return statelessGas + wanted, nil + wanted := evm.AccessEvents.BasicDataGas(beneficiaryAddr, false, contract.Gas.RegularGas-statelessGas, false) + if wanted > contract.Gas.RegularGas-statelessGas { + return GasCosts{RegularGas: statelessGas + wanted}, nil } statelessGas += wanted } // Charge write costs if it transfers value if !balanceIsZero { - wanted := evm.AccessEvents.BasicDataGas(contractAddr, true, contract.Gas-statelessGas, false) - if wanted > contract.Gas-statelessGas { - return statelessGas + wanted, nil + wanted := evm.AccessEvents.BasicDataGas(contractAddr, true, contract.Gas.RegularGas-statelessGas, false) + if wanted > contract.Gas.RegularGas-statelessGas { + return GasCosts{RegularGas: statelessGas + wanted}, nil } statelessGas += wanted if contractAddr != beneficiaryAddr { if evm.StateDB.Exist(beneficiaryAddr) { - wanted = evm.AccessEvents.BasicDataGas(beneficiaryAddr, true, contract.Gas-statelessGas, false) + wanted = evm.AccessEvents.BasicDataGas(beneficiaryAddr, true, contract.Gas.RegularGas-statelessGas, false) } else { - wanted = evm.AccessEvents.AddAccount(beneficiaryAddr, true, contract.Gas-statelessGas) + wanted = evm.AccessEvents.AddAccount(beneficiaryAddr, true, contract.Gas.RegularGas-statelessGas) } - if wanted > contract.Gas-statelessGas { - return statelessGas + wanted, nil + if wanted > contract.Gas.RegularGas-statelessGas { + return GasCosts{RegularGas: statelessGas + wanted}, nil } statelessGas += wanted } } - return statelessGas, nil + return GasCosts{RegularGas: statelessGas}, nil } -func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas, err := gasCodeCopy(evm, contract, stack, mem, memorySize) +func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + gasCost, err := gasCodeCopy(evm, contract, stack, mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } + gas := gasCost.RegularGas if !contract.IsDeployment && !contract.IsSystemCall { var ( codeOffset = stack.Back(1) @@ -175,31 +177,32 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } _, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, length.Uint64()) - _, wanted := evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, contract.Gas-gas) + _, wanted := evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, contract.Gas.RegularGas-gas) gas += wanted } - return gas, nil + return GasCosts{RegularGas: gas}, nil } -func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { // memory expansion first (dynamic part of pre-2929 implementation) - gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) + gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) if err != nil { - return 0, err + return GasCosts{}, err } + gas := gasCost.RegularGas addr := common.Address(stack.peek().Bytes20()) _, isPrecompile := evm.precompile(addr) if isPrecompile || addr == params.HistoryStorageAddress { var overflow bool if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } - wgas := evm.AccessEvents.BasicDataGas(addr, false, contract.Gas-gas, true) + wgas := evm.AccessEvents.BasicDataGas(addr, false, contract.Gas.RegularGas-gas, true) var overflow bool if gas, overflow = math.SafeAdd(gas, wgas); overflow { - return 0, ErrGasUintOverflow + return GasCosts{}, ErrGasUintOverflow } - return gas, nil + return GasCosts{RegularGas: gas}, nil } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index b21e104abc..694debcf98 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -66,9 +66,9 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo tracer.OnTxStart(evm.GetVMContext(), types.NewTx(&types.LegacyTx{Gas: gasLimit, GasPrice: vmctx.txCtx.GasPrice.ToBig()}), contract.Caller()) tracer.OnEnter(0, byte(vm.CALL), contract.Caller(), contract.Address(), []byte{}, startGas, value.ToBig()) ret, err := evm.Run(contract, []byte{}, false) - tracer.OnExit(0, ret, startGas-contract.Gas, err, true) + tracer.OnExit(0, ret, startGas-contract.Gas.RegularGas, err, true) // Rest gas assumes no refund - tracer.OnTxEnd(&types.Receipt{GasUsed: gasLimit - contract.Gas}, nil) + tracer.OnTxEnd(&types.Receipt{GasUsed: gasLimit - contract.Gas.RegularGas}, nil) if err != nil { return nil, err }