From 0c88fee4298b38fd114afe9afc485f1a1d276828 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 8 Jul 2026 23:47:39 +0800 Subject: [PATCH 01/20] eth/downloader, eth/protocols/snap: snap v2 catchup failure (#35321) Fixes https://github.com/ethereum/go-ethereum/issues/35319 --- eth/protocols/snap/syncv2.go | 75 +++++++++++++++++++++++++------ eth/protocols/snap/syncv2_test.go | 21 ++++++--- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/eth/protocols/snap/syncv2.go b/eth/protocols/snap/syncv2.go index 37da0b12b6..63dfc0330d 100644 --- a/eth/protocols/snap/syncv2.go +++ b/eth/protocols/snap/syncv2.go @@ -798,11 +798,22 @@ func catchUpExceedsRetention(prev, curr *types.Header) bool { // diffs to roll flat state forward. func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error { s.lock.RLock() - from := s.pivot.Number.Uint64() + 1 - to := target.Number.Uint64() + prev := s.pivot s.lock.RUnlock() + + var ( + from = prev.Number.Uint64() + 1 + to = target.Number.Uint64() + ) log.Info("Starting BAL catch-up", "from", from, "to", to, "blocks", to-from+1) + // Resolve the hash chain of the whole gap upfront by walking the parent + // hashes backward from the trusted target header. + hashes, err := s.gapHashChain(prev, target) + if err != nil { + return err + } + s.lock.Lock() s.accessListTotal = to - from + 1 s.accessListSynced = 0 @@ -819,26 +830,26 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error { if end > to { end = to } - // Collect block hashes and headers for this window. + // Collect the headers for this window. var ( - hashes = make([]common.Hash, 0, end-start+1) - headers = make(map[common.Hash]*types.Header, end-start+1) + winHashes = hashes[start-from : end-from+1] + headers = make(map[common.Hash]*types.Header, len(winHashes)) ) - for num := start; num <= end; num++ { - hash := rawdb.ReadCanonicalHash(s.db, num) - if hash == (common.Hash{}) { - return fmt.Errorf("missing canonical hash for block %d during catch-up", num) + for i, hash := range winHashes { + num := start + uint64(i) + if num == to { + headers[hash] = target + continue } - header := rawdb.ReadHeader(s.db, hash, num) + header := s.gapHeader(num, hash) if header == nil { return fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, hash) } - hashes = append(hashes, hash) headers[hash] = header } // Fetch this window's BALs from peers. - rawBALs, err := s.fetchAccessLists(hashes, headers, cancel) + rawBALs, err := s.fetchAccessLists(winHashes, headers, cancel) if err != nil { return err } @@ -851,7 +862,7 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error { default: } num := start + uint64(i) - hash := hashes[i] + hash := winHashes[i] // Decode the raw RLP into a BAL. var ( @@ -883,6 +894,44 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error { return nil } +// gapHashChain resolves the hashes of the catch-up gap (prev, target] by +// walking the parent hashes backward from the trusted target header. +// Out-of-memory is not expected as only the 32 bytes hashes are kept. +func (s *syncerV2) gapHashChain(prev, target *types.Header) ([]common.Hash, error) { + var ( + from = prev.Number.Uint64() + 1 + to = target.Number.Uint64() + hashes = make([]common.Hash, to-from+1) + want = target.ParentHash + ) + hashes[to-from] = target.Hash() + for num := to - 1; num >= from; num-- { + header := s.gapHeader(num, want) + if header == nil { + return nil, fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, want) + } + hashes[num-from] = want + want = header.ParentHash + } + if want != prev.Hash() { + return nil, fmt.Errorf("catch-up target %d (hash %v) does not descend from pivot %d (hash %v)", to, target.Hash(), prev.Number, prev.Hash()) + } + return hashes, nil +} + +// gapHeader reads the header of a gap block with the given number and +// expected hash. Two data sources are checked: skeleton or the canonical +// header, as the headers will be kept in skeleton until the full block +// is fully downloaded and inserted. +func (s *syncerV2) gapHeader(num uint64, hash common.Hash) *types.Header { + if header := rawdb.ReadSkeletonHeader(s.db, num); header != nil && header.Hash() == hash { + return header + } + // If the header is outside the skeleton scope, read it from + // canonical source. + return rawdb.ReadHeader(s.db, hash, num) +} + // fetchAccessLists fetches BALs for the given block hashes from // remote peers. It runs its own event loop to assign requests // to idle peers and process responses asynchronously. Each BAL is verified diff --git a/eth/protocols/snap/syncv2_test.go b/eth/protocols/snap/syncv2_test.go index 9e366b108c..577504152f 100644 --- a/eth/protocols/snap/syncv2_test.go +++ b/eth/protocols/snap/syncv2_test.go @@ -1797,8 +1797,11 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) { // Build three sequential BAL blocks (A+1, A+2, A+3). The first two touch // goodAddr, the third touches corruptAddr so that block's apply fails - // once we've corrupted that account's snapshot. + // once we've corrupted that account's snapshot. The headers link up via + // their parent hashes, as the catch-up walks the chain backward from the + // target down to the previous pivot. blocks := make([]balBlock, 3) + parent := pivotAHeader.Hash() for i := 0; i < 3; i++ { blockNum := numA + uint64(i) + 1 target := goodAddr @@ -1819,7 +1822,8 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) { } balHash := b.Hash() header := &types.Header{ - Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0, + ParentHash: parent, + Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0, BaseFee: common.Big0, WithdrawalsHash: &emptyHash, BlobGasUsed: &zero, ExcessBlobGas: &zero, ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash, @@ -1828,6 +1832,7 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) { rawdb.WriteHeader(db, header) rawdb.WriteCanonicalHash(db, header.Hash(), blockNum) blocks[i] = balBlock{header: header, bal: buf.Bytes()} + parent = header.Hash() } // First sync: complete sync to A so persisted state has pivot=A, @@ -1929,12 +1934,15 @@ func testCatchUpWindowed(t *testing.T, scheme string) { rawdb.WriteCanonicalHash(db, pivotA.Hash(), numA) // Build a 5-block gap, each block bumping targetAddr's balance. The last - // block's balance is the expected final state. + // block's balance is the expected final state. The headers link up via + // their parent hashes, as the catch-up walks the chain backward from the + // target down to the previous pivot. const gap = 5 var ( lastHeader *types.Header lastBalance *uint256.Int balsByHash = make(map[common.Hash]rlp.RawValue, gap) + parent = pivotA.Hash() ) for i := 0; i < gap; i++ { blockNum := numA + uint64(i) + 1 @@ -1952,7 +1960,8 @@ func testCatchUpWindowed(t *testing.T, scheme string) { } balHash := b.Hash() header := &types.Header{ - Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0, + ParentHash: parent, + Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0, BaseFee: common.Big0, WithdrawalsHash: &emptyHash, BlobGasUsed: &zero, ExcessBlobGas: &zero, ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash, @@ -1962,6 +1971,7 @@ func testCatchUpWindowed(t *testing.T, scheme string) { rawdb.WriteCanonicalHash(db, header.Hash(), blockNum) balsByHash[header.Hash()] = buf.Bytes() lastHeader, lastBalance = header, balance + parent = header.Hash() } // Seed sync to A: persisted state ends with pivot=A and full flat state. @@ -3008,7 +3018,8 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) { rawdb.WriteCanonicalHash(db, hdrA.Hash(), numA) hdrB := &types.Header{ - Number: new(big.Int).SetUint64(numA + 1), Root: rootB, Difficulty: common.Big0, + ParentHash: hdrA.Hash(), + Number: new(big.Int).SetUint64(numA + 1), Root: rootB, Difficulty: common.Big0, BaseFee: common.Big0, WithdrawalsHash: &emptyH, BlobGasUsed: &zero, ExcessBlobGas: &zero, ParentBeaconRoot: &emptyH, RequestsHash: &emptyH, From 2133e014ae24553561066cd1c4677eeff5f986a8 Mon Sep 17 00:00:00 2001 From: ozpool <151670776+ozpool@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:27:31 +0530 Subject: [PATCH 02/20] internal/ethapi: reject gasPrice together with authorizationList (#35320) Closes #35314. ### Problem `TransactionArgs.ToTransaction` selects `SetCodeTxType` when an `authorizationList` is present, but then unconditionally downgrades to `LegacyTxType` when `gasPrice` is set: ```go case args.AuthorizationList != nil || defaultType == types.SetCodeTxType: usedType = types.SetCodeTxType ... if args.GasPrice != nil { usedType = types.LegacyTxType } ``` A legacy transaction cannot carry an EIP-7702 authorization list, so the list is silently dropped. `eth_sendTransaction`, `eth_signTransaction` and `eth_fillTransaction` can then return a plain legacy transaction/hash even though the requested delegation update or revocation was never included. The downgrade also masks a latent issue: `setFeeDefaults` returns early once `gasPrice` is set (without `authorizationList`) and never fills `MaxFeePerGas`/`MaxPriorityFeePerGas`. Building the `SetCodeTx` without the downgrade would hit `uint256.MustFromBig((*big.Int)(args.MaxFeePerGas))` with a nil fee cap and panic. Erroring early is therefore the correct behavior. ### Fix Reject the `gasPrice` + `authorizationList` combination in `setFeeDefaults`, mirroring the existing `gasPrice` / EIP-1559 guard, so the request fails explicitly instead of producing a transaction that omits the authorization list. ### Test Added a `setFeeDefaults` case asserting the new error. Verified fail-on-main: with the guard reverted the test fails (`expected error: both gasPrice and authorizationList specified`); with the guard it passes. Full `internal/ethapi` package, `go vet` and `gofmt` are clean. ### Note The same silent-drop applies to `gasPrice` + `blobVersionedHashes` (blob transactions also cannot be legacy). I scoped this PR to the reported `authorizationList` case; happy to extend the guard to blob hashes in the same spot if preferred. --- internal/ethapi/transaction_args.go | 6 ++++++ internal/ethapi/transaction_args_test.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 1a7cf1c118..586df48759 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -195,6 +195,12 @@ func (args *TransactionArgs) setFeeDefaults(ctx context.Context, b Backend, head if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") } + // An EIP-7702 set-code transaction cannot be a legacy transaction, so gasPrice + // is incompatible with an authorization list. Reject the combination instead of + // silently dropping the authorization list in ToTransaction. + if args.GasPrice != nil && args.AuthorizationList != nil { + return errors.New("both gasPrice and authorizationList specified") + } // If the tx has completely specified a fee mechanism, no default is needed. // This allows users who are not yet synced past London to get defaults for // other tx values. See https://github.com/ethereum/go-ethereum/pull/23274 diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index f3fc16dcbb..1d587032bb 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -58,6 +58,7 @@ func TestSetFeeDefaults(t *testing.T) { fortytwo = (*hexutil.Big)(big.NewInt(42)) maxFee = (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(b.current.BaseFee, big.NewInt(2)), fortytwo.ToInt())) al = &types.AccessList{types.AccessTuple{Address: common.Address{0xaa}, StorageKeys: []common.Hash{{0x01}}}} + authList = []types.SetCodeAuthorization{{Address: common.Address{0xbb}}} ) tests := []test{ @@ -208,6 +209,13 @@ func TestSetFeeDefaults(t *testing.T) { nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified"), }, + { + "set gas price and authorization list", + "london", + &TransactionArgs{GasPrice: fortytwo, AuthorizationList: authList}, + nil, + errors.New("both gasPrice and authorizationList specified"), + }, // EIP-4844 { "set gas price and maxFee for blob transaction", From 76e3dc6b5be4ab2d1bb75b0b2b0237703a9b8e8a Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 9 Jul 2026 05:44:34 +0800 Subject: [PATCH 03/20] core: improve chain reset head (#35252) This PR does a few things: - reject `debug_setHead` if the target is even before the pivot block (if non-nil) - reject `debug_setHead` if in path mode, the target is not recoverable - decouple the chain rewinding and state recovery in path mode and recover the state in one shot --------- Co-authored-by: jonny rhea <5555162+jrhea@users.noreply.github.com> --- core/blockchain.go | 60 +++++++++++++++++++++------------- core/blockchain_reader.go | 4 +-- core/rawdb/accessors_chain.go | 4 +-- core/rawdb/schema.go | 2 +- eth/api_backend.go | 19 +++++++++++ eth/downloader/beaconsync.go | 4 +-- eth/downloader/downloader.go | 4 +-- eth/downloader/syncmode.go | 8 ++--- triedb/pathdb/database.go | 18 +++++----- triedb/pathdb/history_state.go | 27 --------------- 10 files changed, 78 insertions(+), 72 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index d6edd90133..798d3f1ae6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -913,7 +913,7 @@ func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*typ // noState represents if the target state requested for search // is unavailable and impossible to be recovered. - noState = !bc.HasState(root) && !bc.stateRecoverable(root) + noState = !bc.HasState(root) && !bc.StateRecoverable(root) start = time.Now() // Timestamp the rewinding is restarted logged = time.Now() // Timestamp last progress log was printed @@ -940,7 +940,7 @@ func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*typ } // Check if the associated state is available or recoverable if // the requested root has already been crossed. - if beyondRoot && (bc.HasState(head.Root) || bc.stateRecoverable(head.Root)) { + if beyondRoot && (bc.HasState(head.Root) || bc.StateRecoverable(head.Root)) { break } // If pivot block is reached, return the genesis block as the @@ -966,14 +966,11 @@ func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*typ return head, rootNumber } } - // Recover if the target state if it's not available yet. - if !bc.HasState(head.Root) { - if err := bc.triedb.Recover(head.Root); err != nil { - log.Error("Failed to rollback state, resetting to genesis", "err", err) - return bc.genesisBlock.Header(), rootNumber - } - } - log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash()) + // Note, the state of the located head may not be physically present yet if + // it's only recoverable. The actual recovery is intentionally deferred once + // the new head is finalized, so that a deep rewind rolls the state back in + // one shot. + log.Info("Rewound to block with available state", "number", head.Number, "hash", head.Hash()) return head, rootNumber } @@ -1034,17 +1031,9 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.currentBlock.Store(newHeadBlock) headBlockGauge.Update(int64(newHeadBlock.Number.Uint64())) - // The head state is missing, which is only possible in the path-based - // scheme. This situation occurs when the chain head is rewound below - // the pivot point. In this scenario, there is no possible recovery - // approach except for rerunning a snap sync. Do nothing here until the - // state syncer picks it up. - if !bc.HasState(newHeadBlock.Root) { - if newHeadBlock.Number.Uint64() != 0 { - log.Crit("Chain is stateless at a non-genesis block") - } - log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash()) - } + // Note, the located head state might not be physically present yet; in + // the path-based scheme a recoverable state is materialized in a single + // shot once the rewind is finalized. } // Rewind the snap block in a simpleton way to the target head if currentSnapBlock := bc.CurrentSnapBlock(); currentSnapBlock != nil && header.Number.Uint64() < currentSnapBlock.Number.Uint64() { @@ -1113,6 +1102,31 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.hc.SetHead(head, updateFn, delFn) } } + // In the path-based scheme, the rewind loop above only locates the new head + // without materializing its state, so the potentially deep rollback is done + // here in a single shot. This rolls back the whole rewound range at once, + // performing a single fsync rather than one per block, which is critical when + // rewinding a large number of blocks. + if newHeadBlock := bc.CurrentBlock(); !bc.HasState(newHeadBlock.Root) { + switch { + case bc.StateRecoverable(newHeadBlock.Root): + if err := bc.triedb.Recover(newHeadBlock.Root); err != nil { + // The state was confirmed recoverable just above, so a failure here + // can only stem from an unexpected I/O error. There is no safe way to + // continue with a half-rolled-back state, hence crash hard. + log.Crit("Failed to recover state", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash(), "err", err) + } + case newHeadBlock.Number.Uint64() != 0: + // rewindHead only returns a non-genesis head when its state is present + // or recoverable, so this branch should be unreachable. + log.Crit("Chain is stateless at a non-genesis block", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash()) + default: + // The chain head was rewound below the snap-sync pivot to a stateless + // genesis. There is no recovery approach except rerunning a snap sync; + // do nothing here until the state syncer picks it up. + log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash()) + } + } // Clear out any stale content from the caches bc.bodyCache.Purge() bc.bodyRLPCache.Purge() @@ -2402,7 +2416,7 @@ func (bc *BlockChain) insertSideChain(ctx context.Context, block *types.Block, i ) parent := it.previous() for parent != nil && !bc.HasState(parent.Root) { - if bc.stateRecoverable(parent.Root) { + if bc.StateRecoverable(parent.Root) { if err := bc.triedb.Recover(parent.Root); err != nil { return nil, 0, err } @@ -2464,7 +2478,7 @@ func (bc *BlockChain) recoverAncestors(ctx context.Context, block *types.Block, parent = block ) for parent != nil && !bc.HasState(parent.Root()) { - if bc.stateRecoverable(parent.Root()) { + if bc.StateRecoverable(parent.Root()) { if err := bc.triedb.Recover(parent.Root()); err != nil { return common.Hash{}, err } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index a540bbc11d..217a384752 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -395,11 +395,11 @@ func (bc *BlockChain) HasBlockAndState(hash common.Hash, number uint64) bool { return bc.HasState(block.Root()) } -// stateRecoverable checks if the specified state is recoverable. +// StateRecoverable checks if the specified state is recoverable. // Note, this function assumes the state is not present, because // state is not treated as recoverable if it's available, thus // false will be returned in this case. -func (bc *BlockChain) stateRecoverable(root common.Hash) bool { +func (bc *BlockChain) StateRecoverable(root common.Hash) bool { if bc.triedb.Scheme() == rawdb.HashScheme { return false } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index d8825b6b8f..e82a15877e 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -176,8 +176,8 @@ func WriteFinalizedBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { // ReadLastPivotNumber retrieves the number of the last pivot block. If the node // has never attempted snap sync, the last pivot will always be nil. The marker -// is written during snap sync and never cleared, so that a rollback past the -// pivot can re-enable snap sync. +// is written during snap sync and never cleared, so that a rewind below the +// pivot can be detected. func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 { data, _ := db.Get(lastPivotKey) if len(data) == 0 { diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 54c76143b4..ec72294354 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -46,7 +46,7 @@ var ( // persistentStateIDKey tracks the id of latest stored state(for path-based only). persistentStateIDKey = []byte("LastStateID") - // lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead). + // lastPivotKey tracks the last pivot block used by snap sync (to reject sethead below it). lastPivotKey = []byte("LastPivot") // fastTrieProgressKey tracks the number of trie entries imported during fast sync. diff --git a/eth/api_backend.go b/eth/api_backend.go index d527d4756e..c4e2508a3d 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -19,6 +19,7 @@ package eth import ( "context" "errors" + "fmt" "math/big" "time" @@ -64,6 +65,24 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header { } func (b *EthAPIBackend) SetHead(number uint64) error { + // Reject rewinding to a point before the snap-sync pivot. The earliest + // recoverable state is the pivot block, so a target below it would simply + // reset the chain to genesis, which is almost never the intent behind a + // manual debug_setHead. + if pivot := rawdb.ReadLastPivotNumber(b.eth.ChainDb()); pivot != nil && number < *pivot { + return fmt.Errorf("rewind target %d is before the snap-sync pivot %d", number, *pivot) + } + // In path mode the deepest reachable state is bounded by the amount of state + // histories retained. If the reverse diffs for the target have already been + // pruned, its state is no longer recoverable. + bc := b.eth.blockchain + if bc.TrieDB().Scheme() == rawdb.PathScheme { + if header := bc.GetHeaderByNumber(number); header != nil { + if !bc.HasState(header.Root) && !bc.StateRecoverable(header.Root) { + return errors.New("rewind target is not recoverable") + } + } + } b.eth.handler.downloader.Cancel() return b.eth.blockchain.SetHead(number) } diff --git a/eth/downloader/beaconsync.go b/eth/downloader/beaconsync.go index 246fe7637b..2905b664ed 100644 --- a/eth/downloader/beaconsync.go +++ b/eth/downloader/beaconsync.go @@ -333,8 +333,8 @@ func (d *Downloader) fetchHeaders(from uint64) error { return errNoPivotHeader } // Write out the pivot into the database so a rollback beyond - // it will reenable snap sync and update the state root that - // the state syncer will be downloading + // it can be detected, and update the state root that the + // state syncer will be downloading rawdb.WriteLastPivotNumber(d.stateDB, d.pivotHeader.Number.Uint64()) } } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 60b04e945b..876115ea97 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -542,8 +542,8 @@ func (d *Downloader) syncToHead() (err error) { if pivotNumber <= origin { origin = pivotNumber - 1 } - // Write out the pivot into the database so a rollback beyond it will - // reenable snap sync + // Write out the pivot into the database so a rollback beyond it + // can be detected rawdb.WriteLastPivotNumber(d.stateDB, pivotNumber) } } diff --git a/eth/downloader/syncmode.go b/eth/downloader/syncmode.go index 036119ce3d..a7792eb5fc 100644 --- a/eth/downloader/syncmode.go +++ b/eth/downloader/syncmode.go @@ -40,8 +40,8 @@ func newSyncModer(mode ethconfig.SyncMode, chain BlockChain, disk ethdb.KeyValue // The database seems empty as the current block is the genesis. Yet the snap // block is ahead, so snap sync was enabled for this node at a certain point. // The scenarios where this can happen is - // * if the user manually (or via a bad block) rolled back a snap sync node - // below the sync point. + // * if an internal reset (a fork config change or corruption recovery) + // rolled a snap sync node back below the sync point. // * the last snap sync is not finished while user specifies a full sync this // time. But we don't have any recent state for full sync. // In these cases however it's safe to reenable snap sync. @@ -87,8 +87,8 @@ func (m *syncModer) get(report bool) ethconfig.SyncMode { if report { logger = log.Info } - // We are probably in full sync, but we might have rewound to before the - // snap sync pivot, check if we should re-enable snap sync. + // We are probably in full sync, but an internal reset may have rewound the + // chain below the snap sync pivot, check if we should re-enable snap sync. head := m.chain.CurrentBlock() if pivot := rawdb.ReadLastPivotNumber(m.disk); pivot != nil { if head.Number.Uint64() < *pivot { diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index e52949c93e..996d3f3ce1 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -543,15 +543,15 @@ func (db *Database) Recoverable(root common.Hash) bool { if db.stateFreezer == nil { return false } - // Ensure the requested state is a canonical state and all state - // histories in range [id+1, dl.ID] are present and complete. - return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error { - if m.parent != root { - return errors.New("unexpected state history") - } - root = m.root - return nil - }) == nil + blob := rawdb.ReadStateHistoryMeta(db.stateFreezer, *id+1) + if len(blob) == 0 { + return false // pruned from the tail or otherwise unavailable + } + var m meta + if err := m.decode(blob); err != nil { + return false + } + return m.parent == root } // Close closes the trie database and the held freezer. diff --git a/triedb/pathdb/history_state.go b/triedb/pathdb/history_state.go index 23428b1a54..9d3bf23e22 100644 --- a/triedb/pathdb/history_state.go +++ b/triedb/pathdb/history_state.go @@ -612,30 +612,3 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error { return nil } - -// checkStateHistories retrieves a batch of metadata objects with the specified -// range and performs the callback on each item. -func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error { - for count > 0 { - number := count - if number > 10000 { - number = 10000 // split the big read into small chunks - } - blobs, err := rawdb.ReadStateHistoryMetaList(reader, start, number) - if err != nil { - return err - } - for _, blob := range blobs { - var dec meta - if err := dec.decode(blob); err != nil { - return err - } - if err := check(&dec); err != nil { - return err - } - } - count -= uint64(len(blobs)) - start += uint64(len(blobs)) - } - return nil -} From 6c80ee6c50dc6cd9b35386c913ed51a5e02fd13d Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 9 Jul 2026 10:52:28 +0800 Subject: [PATCH 04/20] eth/protocols/snap: fix access list sync progress double counting (#35323) --- eth/protocols/snap/syncv2.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eth/protocols/snap/syncv2.go b/eth/protocols/snap/syncv2.go index 63dfc0330d..630a9d1b75 100644 --- a/eth/protocols/snap/syncv2.go +++ b/eth/protocols/snap/syncv2.go @@ -963,6 +963,7 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has accessListReqFails = make(chan *accessListRequest) accessListResps = make(chan *accessListResponse) lastStallLog = time.Now() + lastFetched = 0 ) for len(fetched) < len(hashes) { if err := s.checkAccessListProgress(pending, refused); err != nil { @@ -1000,7 +1001,8 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has s.processAccessListResponse(res, headers, pending, fetched, refused) } s.lock.Lock() - s.accessListSynced += uint64(len(fetched)) + s.accessListSynced += uint64(len(fetched) - lastFetched) + lastFetched = len(fetched) s.refreshProgressLocked() s.lock.Unlock() } From 896a4ab7c64e01df188caca6f896243cf26d7095 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 9 Jul 2026 12:21:15 +0800 Subject: [PATCH 05/20] core/vm/runtime: cap regular gas budget at MaxTxGas for Amsterdam (#35301) --- core/vm/runtime/runtime.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 9a15f7ac96..8dfa0fb740 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -143,12 +143,16 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { cfg.State.CreateAccount(address) // set the receiver's (the executing contract) code for execution. cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) + limit := cfg.GasLimit + if rules.IsAmsterdam { + limit = min(cfg.GasLimit, params.MaxTxGas) + } // Call the code with the given configuration. ret, result, err := vmenv.Call( cfg.Origin, common.BytesToAddress([]byte("contract")), input, - vm.NewGasBudget(cfg.GasLimit, 0), + vm.NewGasBudget(limit, cfg.GasLimit-limit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { @@ -178,11 +182,15 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { // - prepare accessList(post-berlin) // - reset transient storage(eip 1153) cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) + limit := cfg.GasLimit + if rules.IsAmsterdam { + limit = min(cfg.GasLimit, params.MaxTxGas) + } // Call the code with the given configuration. code, address, result, _, err := vmenv.Create( cfg.Origin, input, - vm.NewGasBudget(cfg.GasLimit, 0), + vm.NewGasBudget(limit, cfg.GasLimit-limit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { @@ -212,12 +220,16 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er // - reset transient storage(eip 1153) statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil) + limit := cfg.GasLimit + if rules.IsAmsterdam { + limit = min(cfg.GasLimit, params.MaxTxGas) + } // Call the code with the given configuration. ret, result, err := vmenv.Call( cfg.Origin, address, input, - vm.NewGasBudget(cfg.GasLimit, 0), + vm.NewGasBudget(limit, cfg.GasLimit-limit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { From 2ce0200762eb4ea77bb02a033004b4d9f75bec8c Mon Sep 17 00:00:00 2001 From: spencer Date: Thu, 9 Jul 2026 09:27:34 +0100 Subject: [PATCH 06/20] cmd/evm, core: fixes for eels tests@v20.0.0 release (#35283) ## Summary Sanity fixes surfaced by running the EELS `tests@v20.0.0` fixture release (63,109 blockchain tests) through `evm blocktest`. Also bumps CI to consume the new release: `build/checksums.txt` now points at `tests@v20.0.0` / `fixtures.tar.gz` from `ethereum/execution-specs` (supersedes the archived EEST repo's `fixtures_develop`). --------- Co-authored-by: Gary Rong --- build/checksums.txt | 8 ++-- build/ci.go | 2 +- tests/block_test.go | 11 +++--- tests/gen_stenv.go | 6 +++ tests/init.go | 82 +++++++++++++++++++++++++-------------- tests/state_test.go | 6 +-- tests/state_test_util.go | 3 ++ tests/transaction_test.go | 2 +- 8 files changed, 74 insertions(+), 46 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index 454efa93c4..0d135b93e5 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -1,9 +1,9 @@ # This file contains sha256 checksums of optional build dependencies. -# version:spec-tests v5.1.0 -# https://github.com/ethereum/execution-spec-tests/releases -# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 -a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz +# version:spec-tests tests@v20.0.0 +# https://github.com/ethereum/execution-specs/releases +# https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0 +b183702a5b447b465873865357ced9eb342315922e52e31b714e2de115dd0bb4 fixtures.tar.gz # version:golang 1.25.10 # https://go.dev/dl/ diff --git a/build/ci.go b/build/ci.go index 53ade2e1bf..320e858e82 100644 --- a/build/ci.go +++ b/build/ci.go @@ -452,7 +452,7 @@ func doTest(cmdline []string) { // downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures. func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { ext := ".tar.gz" - base := "fixtures_develop" + base := "fixtures" archivePath := filepath.Join(cachedir, base+ext) if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { log.Fatal(err) diff --git a/tests/block_test.go b/tests/block_test.go index 0f087967bb..8c1a7ace0f 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -88,14 +88,13 @@ func TestExecutionSpecBlocktests(t *testing.T) { bt := new(testMatcher) // These tests require us to handle scenarios where a system contract is not deployed at a fork - bt.skipLoad(".*prague/eip7251_consolidations/test_system_contract_deployment.json") - bt.skipLoad(".*prague/eip7002_el_triggerable_withdrawals/test_system_contract_deployment.json") + bt.skipLoad(`.*eip7251_consolidations/contract_deployment/system_contract_deployment\.json`) + bt.skipLoad(`.*eip7002_el_triggerable_withdrawals/contract_deployment/system_contract_deployment\.json`) // Broken tests - bt.skipLoad(`RevertInCreateInInit`) - bt.skipLoad(`InitCollisionParis`) - bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`) - bt.skipLoad(`create2collisionStorageParis`) + bt.skipLoad(`.*eip7610_create_collision/initcollision/.*`) + bt.skipLoad(`.*eip7610_create_collision/revert_in_create/.*`) + bt.skipLoad(`.*stRandom2/random_statetest642/.*`) bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { execBlockTest(t, bt, test) diff --git a/tests/gen_stenv.go b/tests/gen_stenv.go index a5bd0d5fcb..89d533f9d6 100644 --- a/tests/gen_stenv.go +++ b/tests/gen_stenv.go @@ -24,6 +24,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber" gencodec:"optional"` } var enc stEnv enc.Coinbase = common.UnprefixedAddress(s.Coinbase) @@ -34,6 +35,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { enc.Timestamp = math.HexOrDecimal64(s.Timestamp) enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee) enc.ExcessBlobGas = (*math.HexOrDecimal64)(s.ExcessBlobGas) + enc.SlotNumber = (*math.HexOrDecimal64)(s.SlotNumber) return json.Marshal(&enc) } @@ -48,6 +50,7 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber" gencodec:"optional"` } var dec stEnv if err := json.Unmarshal(input, &dec); err != nil { @@ -81,5 +84,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { s.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.SlotNumber != nil { + s.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/tests/init.go b/tests/init.go index 67f199203f..19cfdba33b 100644 --- a/tests/init.go +++ b/tests/init.go @@ -487,7 +487,7 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, }, }, "OsakaToBPO1AtTime15k": { @@ -515,7 +515,7 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, }, }, "BPO2": { @@ -544,8 +544,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, }, }, "BPO1ToBPO2AtTime15k": { @@ -574,8 +574,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, }, }, "BPO3": { @@ -605,8 +605,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, }, }, @@ -637,8 +637,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, }, }, @@ -670,8 +670,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, BPO4: params.DefaultBPO4BlobConfig, }, @@ -704,8 +704,8 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, BPO4: params.DefaultBPO4BlobConfig, }, @@ -732,17 +732,44 @@ var Forks = map[string]*params.ChainConfig{ 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, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, - BPO3: params.DefaultBPO3BlobConfig, - BPO4: params.DefaultBPO4BlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, + }, + }, + "BPO2ToAmsterdamAtTime15k": { + 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), + AmsterdamTime: u64(15_000), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, }, }, "Verkle": { @@ -793,16 +820,11 @@ var Forks = map[string]*params.ChainConfig{ }, } -var bpo1BlobConfig = ¶ms.BlobConfig{ - Target: 9, - Max: 14, - UpdateFraction: 8832827, -} - -var bpo2BlobConfig = ¶ms.BlobConfig{ - Target: 14, - Max: 21, - UpdateFraction: 13739630, +func init() { + // Execution-spec-tests fixtures use the historical upgrade names for + // the EIP150 and EIP158 rulesets. + Forks["TangerineWhistle"] = Forks["EIP150"] + Forks["SpuriousDragon"] = Forks["EIP158"] } // AvailableForks returns the set of defined fork names diff --git a/tests/state_test.go b/tests/state_test.go index 09c5cad40e..9b8aa61bc6 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -97,10 +97,8 @@ func TestExecutionSpecState(t *testing.T) { st := new(testMatcher) // Broken tests - st.skipLoad(`RevertInCreateInInit`) - st.skipLoad(`InitCollisionParis`) - st.skipLoad(`dynamicAccountOverwriteEmpty_Paris`) - st.skipLoad(`create2collisionStorageParis`) + st.skipLoad(`.*eip7610_create_collision/initcollision/.*`) + st.skipLoad(`.*eip7610_create_collision/revert_in_create/.*`) st.walk(t, executionSpecStateTestDir, func(t *testing.T, name string, test *StateTest) { execStateTest(t, st, test) diff --git a/tests/state_test_util.go b/tests/state_test_util.go index e33e15fc8c..c41e680463 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -95,6 +95,7 @@ type stEnv struct { Timestamp uint64 `json:"currentTimestamp" gencodec:"required"` BaseFee *big.Int `json:"currentBaseFee" gencodec:"optional"` ExcessBlobGas *uint64 `json:"currentExcessBlobGas" gencodec:"optional"` + SlotNumber *uint64 `json:"slotNumber" gencodec:"optional"` } type stEnvMarshaling struct { @@ -106,6 +107,7 @@ type stEnvMarshaling struct { Timestamp math.HexOrDecimal64 BaseFee *math.HexOrDecimal256 ExcessBlobGas *math.HexOrDecimal64 + SlotNumber *math.HexOrDecimal64 } //go:generate go run github.com/fjl/gencodec -type stTransaction -field-override stTransactionMarshaling -out gen_sttransaction.go @@ -378,6 +380,7 @@ func (t *StateTest) genesis(config *params.ChainConfig) *core.Genesis { GasLimit: t.json.Env.GasLimit, Number: t.json.Env.Number, Timestamp: t.json.Env.Timestamp, + SlotNumber: t.json.Env.SlotNumber, Alloc: t.json.Pre, } if t.json.Env.Random != nil { diff --git a/tests/transaction_test.go b/tests/transaction_test.go index 73ee3aa16a..da9de48bc8 100644 --- a/tests/transaction_test.go +++ b/tests/transaction_test.go @@ -70,7 +70,7 @@ func TestExecutionSpecTransaction(t *testing.T) { st := new(testMatcher) // Emptiness of authorization list is only validated during the tx precheck - st.skipLoad("^prague/eip7702_set_code_tx/test_empty_authorization_list.json") + st.skipLoad(`eip7702_set_code_tx/invalid_tx/empty_authorization_list\.json`) st.walk(t, executionSpecTransactionTestDir, func(t *testing.T, name string, test *TransactionTest) { if err := st.checkFailure(t, test.Run()); err != nil { From 5a39aa10adf92e1b0c76d9349ef47e16511e994e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Kj=C3=A6rstad?= Date: Thu, 9 Jul 2026 16:50:42 +0200 Subject: [PATCH 07/20] build: upgrade -dlgo version to Go 1.25.12 (#35317) New security fix: https://groups.google.com/g/golang-announce/c/OrmQE_Yp5Sc --- build/checksums.txt | 84 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index 0d135b93e5..e970236db0 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,49 +5,49 @@ # https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0 b183702a5b447b465873865357ced9eb342315922e52e31b714e2de115dd0bb4 fixtures.tar.gz -# version:golang 1.25.10 +# version:golang 1.25.12 # https://go.dev/dl/ -20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz -a194e767c2ab4216a60acc068b9dbe6bf4fae05c14bb52d6bbdcb5b3ea521308 go1.25.10.aix-ppc64.tar.gz -52321165a3146cd91865ef98371506a846ed4dc4f9f1c9323e5ad90d2a411e06 go1.25.10.darwin-amd64.tar.gz -795691a425de7e7cdba3544f354dcd2cebcf52e87dc6898193878f34eb6d634f go1.25.10.darwin-arm64.tar.gz -e37b4544ba9e9e9a7ab2ed3116b3fc4d39a88da854baa5a566d9d6d3a9de7d4c go1.25.10.dragonfly-amd64.tar.gz -2a70d1fdabab637aa442ca94599a56e381238efa20cb995d5433b8579bfe482c go1.25.10.freebsd-386.tar.gz -9cdf522d87d47d82fec4a313cc4f8c3c94a7770426e8d443e4150a1f330cba71 go1.25.10.freebsd-amd64.tar.gz -6da6183633e9e59ffd9edefab68b5059c89b605596d94aaba650b1681fccd35f go1.25.10.freebsd-arm.tar.gz -7adcefeebdd05331f4d45f1ad2dddb5c53537cff6552e82f6595b3b833b95371 go1.25.10.freebsd-arm64.tar.gz -285f80a1ace21a7d94035cd753196eeada8cacd48e6396fd116ad5eb67aea957 go1.25.10.freebsd-riscv64.tar.gz -de7461bf0e5068a4f6e7f8713026d70516be6dbd5de5d21f9ced1c182f2f326e go1.25.10.illumos-amd64.tar.gz -2f574f2e2e19ead5b280fec0e7af5c81b76632685f03b6ac42dfa34c4b773c52 go1.25.10.linux-386.tar.gz -42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70 go1.25.10.linux-amd64.tar.gz -654da1f9b50a5d1c2a85ccf8ed405aa89c06e94d18384628bf186f7712677b08 go1.25.10.linux-arm64.tar.gz -39f168f158e693887d3ad006168af1b1a3007b19c5993cae4d9d57f82f52aaf8 go1.25.10.linux-armv6l.tar.gz -05401fe5ea50ad2bafb9c797ef9bf21574b0661f19ef4d0dd66af8a0fb7323f3 go1.25.10.linux-loong64.tar.gz -d5bc2d6155d394a3aae41f21eb7c60da5595a6147aa0f30ed6b27da25e06c3f7 go1.25.10.linux-mips.tar.gz -8c64e7493e5953c3ba3153487d2fddd7f8ed142392c77f138e6792a6c1930db4 go1.25.10.linux-mips64.tar.gz -bd53aa2d558b7c1eadfc6bf01132e1859203a92f458ed7ba75b7f3230f14b095 go1.25.10.linux-mips64le.tar.gz -120b254e2e2980bb06687175db5c4064a85696c53001dc9f59934ad18f74a6bc go1.25.10.linux-mipsle.tar.gz -8a6acb21295b0ec974a44608361920ea8dbff5666631a6f556bd7d5f1d56535f go1.25.10.linux-ppc64.tar.gz -778925fdcdf9a272f823d147fad51545c3334b7ccd8652b2ccaaf2b01800280a go1.25.10.linux-ppc64le.tar.gz -b4f04ad0db48bcfea946db5323919cd21034e0bd2821a557dacd29c1b1013a4b go1.25.10.linux-riscv64.tar.gz -936b953e43921a64c12da871f76871ebbeb6d2092a7b8bdc307f5246f3c662cc go1.25.10.linux-s390x.tar.gz -061470e0bc7132146a5925a3cc28d5bc498eb1b1ff09dedcfaae10f781ff2274 go1.25.10.netbsd-386.tar.gz -63b2d50d7f8f269a9c82d42a4060e90cffb7f9102299818bb071b067aac8da8f go1.25.10.netbsd-amd64.tar.gz -c35129f68796526aa4dc4b6f481e2d995ef312aedadc88b659b945cc00e1f8f0 go1.25.10.netbsd-arm.tar.gz -2f541da4e2b298154d992d1f11bbb38c89d0821d91cc50a46776d42bb5e63bca go1.25.10.netbsd-arm64.tar.gz -2d42e569b07f1b99fdbfd008e7c22f967d165e2ce02464f46818fbed2aec43f5 go1.25.10.openbsd-386.tar.gz -0ad05960e8c9f867328151308c87f938433bec8f22f6a9437a896e22169fc840 go1.25.10.openbsd-amd64.tar.gz -099cc11473f99461c77161912740945308f08f6834980afb262c72bdc915f2d7 go1.25.10.openbsd-arm.tar.gz -bdf3335d5008c1ddc81fa94892283e4f1fee22566f5351d4e726d9f55a67c838 go1.25.10.openbsd-arm64.tar.gz -0933d418da0a61e0f29de717a77498f16b9b5b50dbe2205e20b2ed7fd4067f75 go1.25.10.openbsd-ppc64.tar.gz -191e6f3e75712f8c13d189d53b668e2cac6449f26474c1d86fbd04f6e9846f9c go1.25.10.openbsd-riscv64.tar.gz -68c053c8acd76c50fc430e92f4a86110ec3d97dd03d27b9339b4eaf793caff5f go1.25.10.plan9-386.tar.gz -42e2c46638ae22d93402e79efb40faee5c42cf7c56a01bb3ab47c6bb2512b745 go1.25.10.plan9-amd64.tar.gz -3ef1d5838b1648da16724a07b72e839ccbd7cb8899c3e0426afd6b79d494b91c go1.25.10.plan9-arm.tar.gz -631e3716017fbec06500a628d97e1155daec3593f0a7812c2ebfe8fc8c96b2ab go1.25.10.solaris-amd64.tar.gz -ddc693d2d9d7cc671ebb72d1d50aa05670f95b059b7d90440611af57976871d5 go1.25.10.windows-386.zip -ca37af2dadd8544464f1a9ca7c3886499d1cdfcb263855d0a1d71f194b2bd222 go1.25.10.windows-amd64.zip -38be57e0398bd93673d65bcae6dc7ee3cf151d7038d0dba5c60a5153022872da go1.25.10.windows-arm64.zip +f90dcee4bd023fa376374ea0a5a6ebe553537b39c426ffd8c689469b45519932 go1.25.12.src.tar.gz +70b4b6509ed60735eff6ed9496824ad9f96201fdf5bd190184123f5908f224ef go1.25.12.aix-ppc64.tar.gz +00a2e743b82bccec03c51c4b0f7e46d5fec52184075fd6c5183c3bb39ae9fb00 go1.25.12.darwin-amd64.tar.gz +fa2c88bbcf64bd3b2aef355f026cfec6d3a4a01c132f999c8f8c964eb767164f go1.25.12.darwin-arm64.tar.gz +2124cfbc1cf0b949eb819478365d4d665f84297a60f327cc65f75f61b2629b96 go1.25.12.dragonfly-amd64.tar.gz +bdb8ab506428e9633653e67c13d582a6230406f01037023b327427c66b058ff2 go1.25.12.freebsd-386.tar.gz +4433a424d466d47d1716df69e6a77c65b1a34d82121488014e4656d73740ebb0 go1.25.12.freebsd-amd64.tar.gz +429fbcb46468a66d1cd24129a1b6163167676aaae8e0989a08ba95ba6aae65c4 go1.25.12.freebsd-arm.tar.gz +c0e31a4cb827fd20fac950bcb4688fd94cdd1491dbdcff7c3754ac8f3b136eb1 go1.25.12.freebsd-arm64.tar.gz +0ea50c6d43e809d46c2c5504e97ef11822f7ca137625171924cda971449b5373 go1.25.12.freebsd-riscv64.tar.gz +d74c214e0c3ae8f9db23f4ec6f6b2f6cb300c3e4b9d840b82fa517af629c455d go1.25.12.illumos-amd64.tar.gz +b71e88ae779850dc2afcd0f2e2208798652311dfda72fdef5d05721d601b8fd3 go1.25.12.linux-386.tar.gz +234828b7a89e0e303d2556310ee549fbcf253d28de937bac3da13d6294262ac1 go1.25.12.linux-amd64.tar.gz +8b5884aef89600aef5b0b051fb971f11f49bb996521e911f30f02a66884f7bd2 go1.25.12.linux-arm64.tar.gz +6cd7311c02c73ba0b482a1cf8c885268edf23519261bf4b5cef3353ad934d1f1 go1.25.12.linux-armv6l.tar.gz +0c6b2b7db8509d1df189433709ddc8fe84c12843e3713508e9ca04dc9e75ddeb go1.25.12.linux-loong64.tar.gz +5abc63471425ab8b3408f596cd547d28d374f6dc860a4cbd6de79497123db4f3 go1.25.12.linux-mips.tar.gz +a18f7a5ac799ed8ac264b63707e7fa475b1b81da6bbdd41c394fcfd69cc6b736 go1.25.12.linux-mips64.tar.gz +5bde8a5d4c05b428fca3e6022dc41d4c735858be9c6945e57473da00fcddd0cc go1.25.12.linux-mips64le.tar.gz +33c243d2b0700589e75a410ebab5b64580a477e908872abc46eef840e6ea535a go1.25.12.linux-mipsle.tar.gz +b5288a7adb38540177146b47aa43bc75f1936c5e14026d6b6e531b534a49eb1a go1.25.12.linux-ppc64.tar.gz +64adb4ddefef4f0a6f11af550547f39bf510350da69ab308438a21eacfde97ad go1.25.12.linux-ppc64le.tar.gz +26919d62d21b0bee9c5c67ad76ace462edd16c2c128a983d942cb45b0bf7693c go1.25.12.linux-riscv64.tar.gz +09875de1d6cb3237437112271b9df3a96bac9fcd10cbf9bc777c00e67f4e3e3d go1.25.12.linux-s390x.tar.gz +2f19f4afc3d6551804228484569ec1ebf4a57a91cda75d746384aa71d5016be7 go1.25.12.netbsd-386.tar.gz +d2237237d34058658187455d4f05f8f4f5f2118b33827308ad8313ab0f00a693 go1.25.12.netbsd-amd64.tar.gz +4d44c8141a829541c3499db6665a47be2c6fdb97903575058809a2a8be53af89 go1.25.12.netbsd-arm.tar.gz +fc96791f8b9cdaea544e827dc15574731afd28b7378053e801b3bd466df6dd9d go1.25.12.netbsd-arm64.tar.gz +e9ced39c191409207a211c6013b9a156bd90dbd5a76ebd8c66dc3bf69bcb2f9e go1.25.12.openbsd-386.tar.gz +bae7c016f0aff806c9af08ecff96b59a232144c96b1d25f4c5f6c85c8e0dbf01 go1.25.12.openbsd-amd64.tar.gz +8517c4bca20e975acdd5a2f5e425cd2bec737bbb6d7ee18d11e3a1545014267e go1.25.12.openbsd-arm.tar.gz +ba4ac29243f43b85a52f12ed7dd02b767e4524785fb6d04fececb7ce125aaa82 go1.25.12.openbsd-arm64.tar.gz +dca067f41d00ba805342062ab21b88831a06a5682e9aaec1c990c175a5656c6b go1.25.12.openbsd-ppc64.tar.gz +4f1f5376464fc91f32cd253dadfe3535ff57d1729b24e6ab014fb83964dfe10f go1.25.12.openbsd-riscv64.tar.gz +c938792ec65ba33592deea6673265d509aaf9b5297119bc91ae973fb6beb62b9 go1.25.12.plan9-386.tar.gz +5aec7ab115c1c6cd049a64f1617ef91d24ea07c8b0f40436d531b42d546f5e37 go1.25.12.plan9-amd64.tar.gz +efe1dcb9750507260a28688a749c72f35c3160646f08a80606f38c3d30d74a12 go1.25.12.plan9-arm.tar.gz +83c42cfedcc0bb03fd601d692d75b085406c9a7f5ff505bf41030f2734b62ed2 go1.25.12.solaris-amd64.tar.gz +18abdf76719f5f84eaa35eeaf87b467a02ed075a8632ad941f10aa7a3d0de713 go1.25.12.windows-386.zip +d5dc82da351b00e5eedd04f41356817d674cc4308131f0f638a5b14c5c3af4cb go1.25.12.windows-amd64.zip +054f046a5fa31fdcc9491cc19065cbf43bf521d805bbe298ae8d65dd981fca84 go1.25.12.windows-arm64.zip # version:golangci 2.10.1 # https://github.com/golangci/golangci-lint/releases/ From 111e7b8b48251aec734e2b361a60dbafd4fda42f Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Thu, 9 Jul 2026 18:54:27 +0200 Subject: [PATCH 08/20] eth: protect high-value peers from random dropping based on tx inclusion stats (#34702) The peer dropper periodically disconnects random peers to create churn. This was previously blind to peer quality. This PR adds peer-score based peer protection, handling the multi-dimensionality problem of peer scoring through the concept of protected peer pools. --------- Signed-off-by: Csaba Kiraly Co-authored-by: healthykim --- eth/backend.go | 5 +- eth/dropper.go | 162 ++++++- eth/dropper_test.go | 234 +++++++++ eth/fetcher/tx_fetcher.go | 25 +- eth/fetcher/tx_fetcher_test.go | 3 +- eth/handler.go | 9 +- eth/txtracker/tracker.go | 320 +++++++++++++ eth/txtracker/tracker_test.go | 501 ++++++++++++++++++++ tests/fuzzers/txfetcher/txfetcher_fuzzer.go | 2 +- 9 files changed, 1225 insertions(+), 36 deletions(-) create mode 100644 eth/dropper_test.go create mode 100644 eth/txtracker/tracker.go create mode 100644 eth/txtracker/tracker_test.go diff --git a/eth/backend.go b/eth/backend.go index 366f911785..72bda3191f 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -473,8 +473,8 @@ func (s *Ethereum) Start() error { // Start the networking layer s.handler.Start(s.p2pServer.MaxPeers) - // Start the connection manager - s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }) + // Start the connection manager with inclusion-based peer protection. + s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }, s.handler.txTracker.GetAllPeerStats) // Subscribe to chain events for the filterMaps head updater. s.fmHeadSub = s.blockchain.SubscribeChainEvent(s.fmHeadEventCh) @@ -600,6 +600,7 @@ func (s *Ethereum) Stop() error { // Stop all the peer-related stuff first. s.discmix.Close() s.dropper.Stop() + s.handler.txTracker.Stop() s.handler.Stop() // Then stop everything else. diff --git a/eth/dropper.go b/eth/dropper.go index 4cbe52ed98..36c970e8d8 100644 --- a/eth/dropper.go +++ b/eth/dropper.go @@ -17,6 +17,7 @@ package eth import ( + "cmp" mrand "math/rand" "slices" "sync" @@ -24,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/eth/txtracker" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" @@ -40,6 +42,10 @@ const ( // dropping when no more peers can be added. Larger numbers result in more // aggressive drop behavior. peerDropThreshold = 0 + // Fraction of inbound/dialed peers to protect based on inclusion stats. + // The top inclusionProtectionFrac of each category (by score) are + // shielded from random dropping. 0.1 = top 10%. + inclusionProtectionFrac = 0.1 ) var ( @@ -47,18 +53,55 @@ var ( droppedInbound = metrics.NewRegisteredMeter("eth/dropper/inbound", nil) // droppedOutbound is the number of outbound peers dropped droppedOutbound = metrics.NewRegisteredMeter("eth/dropper/outbound", nil) + // dropSkipped counts times a drop was attempted but no peer was dropped, + // for any reason (pool has headroom, all candidates trusted/static/young, + // or protected by inclusion stats). + dropSkipped = metrics.NewRegisteredMeter("eth/dropper/skipped", nil) ) -// dropper monitors the state of the peer pool and makes changes as follows: -// - during sync the Downloader handles peer connections, so dropper is disabled -// - if not syncing and the peer count is close to the limit, it drops peers -// randomly every peerDropInterval to make space for new peers -// - peers are dropped separately from the inbound pool and from the dialed pool +// Callback type to get per-peer inclusion statistics. +type getPeerStatsFunc func() map[string]txtracker.PeerStats + +// protectionCategory defines a peer scoring function and the fraction of peers +// to protect per inbound/dialed category. Multiple categories are unioned. +type protectionCategory struct { + score func(txtracker.PeerStats) float64 + frac float64 // fraction of max peers to protect (0.0–1.0) +} + +// protectionCategories is the list of protection criteria. Each category +// independently selects its top-N peers per pool; the union is protected. +var protectionCategories = []protectionCategory{ + {func(s txtracker.PeerStats) float64 { return s.RecentFinalized }, inclusionProtectionFrac}, // Recent finalized + {func(s txtracker.PeerStats) float64 { return s.RecentIncluded }, inclusionProtectionFrac}, // Recent included +} + +// dropper monitors the state of the peer pool and introduces churn by +// periodically disconnecting a random peer to make room for new connections. +// The main goal is to allow new peers to join the network and to facilitate +// continuous topology adaptation. +// +// Behavior: +// - During sync the Downloader handles peer connections, so dropper is disabled. +// - When not syncing and a peer category (inbound or dialed) is close to its +// limit, a random peer from that category is disconnected every 3–7 minutes. +// - Trusted and static peers are never dropped. +// - Recently connected peers are also protected from dropping to give them time +// to prove their value before being at risk of disconnection. +// - Some peers are protected from dropping based on their contribution +// to the tx pool. Each pool (inbound/dialed) independently selects its +// top fraction of peers by a per-peer EMA score — a slow EMA of +// finalized inclusions (~1-day half-life, rewards sustained long-term +// contribution) and a fast EMA of recent block inclusions (rewards +// current activity). The union of all protected sets is shielded from +// random dropping, and the drop target is chosen randomly from the +// remainder. type dropper struct { maxDialPeers int // maximum number of dialed peers maxInboundPeers int // maximum number of inbound peers peersFunc getPeersFunc syncingFunc getSyncingFunc + peerStatsFunc getPeerStatsFunc // optional: inclusion stats for protection // peerDropTimer introduces churn if we are close to limit capacity. // We handle Dialed and Inbound connections separately @@ -88,10 +131,12 @@ func newDropper(maxDialPeers, maxInboundPeers int) *dropper { return cm } -// Start the dropper. -func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc) { +// Start the dropper. peerStatsFunc is optional (nil disables inclusion +// protection). +func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc, peerStatsFunc getPeerStatsFunc) { cm.peersFunc = srv.Peers cm.syncingFunc = syncingFunc + cm.peerStatsFunc = peerStatsFunc cm.wg.Add(1) go cm.loop() } @@ -114,30 +159,101 @@ func (cm *dropper) dropRandomPeer() bool { } numDialed := len(peers) - numInbound + // Fast path: if neither pool is near capacity, every non-trusted/non-static + // peer is already do-not-drop by pool-threshold rules. No point computing + // inclusion protection. + if cm.maxDialPeers-numDialed > peerDropThreshold && + cm.maxInboundPeers-numInbound > peerDropThreshold { + dropSkipped.Mark(1) + return false + } + + // Compute the set of inclusion-protected peers before filtering. + protected := cm.protectedPeers(peers) + selectDoNotDrop := func(p *p2p.Peer) bool { - // Avoid dropping trusted and static peers, or recent peers. - // Only drop peers if their respective category (dialed/inbound) - // is close to limit capacity. return p.Trusted() || p.StaticDialed() || p.Lifetime() < mclock.AbsTime(doNotDropBefore) || (p.DynDialed() && cm.maxDialPeers-numDialed > peerDropThreshold) || - (p.Inbound() && cm.maxInboundPeers-numInbound > peerDropThreshold) + (p.Inbound() && cm.maxInboundPeers-numInbound > peerDropThreshold) || + protected[p] } droppable := slices.DeleteFunc(peers, selectDoNotDrop) - if len(droppable) > 0 { - p := droppable[mrand.Intn(len(droppable))] - log.Debug("Dropping random peer", "inbound", p.Inbound(), - "id", p.ID(), "duration", common.PrettyDuration(p.Lifetime()), "peercountbefore", len(peers)) - p.Disconnect(p2p.DiscUselessPeer) - if p.Inbound() { - droppedInbound.Mark(1) - } else { - droppedOutbound.Mark(1) - } - return true + if len(droppable) == 0 { + dropSkipped.Mark(1) + return false } - return false + p := droppable[mrand.Intn(len(droppable))] + log.Debug("Dropping random peer", "inbound", p.Inbound(), + "id", p.ID(), "duration", common.PrettyDuration(p.Lifetime()), "peercountbefore", len(peers)) + p.Disconnect(p2p.DiscUselessPeer) + if p.Inbound() { + droppedInbound.Mark(1) + } else { + droppedOutbound.Mark(1) + } + return true +} + +// protectedPeers computes the set of peers that should not be dropped based +// on inclusion stats. Each protection category independently selects its +// top-N peers per inbound/dialed pool; the union is returned. +func (cm *dropper) protectedPeers(peers []*p2p.Peer) map[*p2p.Peer]bool { + if cm.peerStatsFunc == nil { + return nil + } + stats := cm.peerStatsFunc() + if len(stats) == 0 { + return nil + } + // Split peers by direction. + var inbound, dialed []*p2p.Peer + for _, p := range peers { + if p.Inbound() { + inbound = append(inbound, p) + } else { + dialed = append(dialed, p) + } + } + result := protectedPeersByPool(inbound, dialed, stats) + if len(result) > 0 { + log.Debug("Protecting high-value peers from drop", "protected", len(result)) + } + return result +} + +// protectedPeersByPool selects the union of top-N peers per protection +// category across the given already-split inbound and dialed pools. +// Factored from protectedPeers so tests can exercise the per-pool +// selection logic without needing to construct direction-flagged +// *p2p.Peer instances (which require unexported p2p types). +func protectedPeersByPool(inbound, dialed []*p2p.Peer, stats map[string]txtracker.PeerStats) map[*p2p.Peer]bool { + result := make(map[*p2p.Peer]bool) + // protectPool selects the top-frac peers from pool by score and adds them to result. + protectPool := func(pool []*p2p.Peer, cat protectionCategory) { + n := int(float64(len(pool)) * cat.frac) + if n == 0 { + return + } + sorted := slices.SortedFunc(slices.Values(pool), func(a, b *p2p.Peer) int { + // descending + scoreB := cat.score(stats[b.ID().String()]) + scoreA := cat.score(stats[a.ID().String()]) + return cmp.Compare(scoreB, scoreA) + }) + // select top n peers excluding 0 + for _, p := range sorted[:min(n, len(sorted))] { + if cat.score(stats[p.ID().String()]) > 0 { + result[p] = true + } + } + } + for _, cat := range protectionCategories { + protectPool(inbound, cat) + protectPool(dialed, cat) + } + return result } // randomDuration generates a random duration between min and max. diff --git a/eth/dropper_test.go b/eth/dropper_test.go new file mode 100644 index 0000000000..fd2ed9b611 --- /dev/null +++ b/eth/dropper_test.go @@ -0,0 +1,234 @@ +// 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 eth + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/eth/txtracker" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +func makePeers(n int) []*p2p.Peer { + peers := make([]*p2p.Peer, n) + for i := range peers { + id := enode.ID{byte(i)} + peers[i] = p2p.NewPeer(id, fmt.Sprintf("peer%d", i), nil) + } + return peers +} + +func TestProtectedPeersNoStats(t *testing.T) { + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return nil } + + peers := makePeers(10) + protected := cm.protectedPeers(peers) + if len(protected) != 0 { + t.Fatalf("expected no protected peers with nil stats, got %d", len(protected)) + } +} + +func TestProtectedPeersEmptyStats(t *testing.T) { + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + cm.peerStatsFunc = func() map[string]txtracker.PeerStats { + return map[string]txtracker.PeerStats{} + } + + peers := makePeers(10) + protected := cm.protectedPeers(peers) + if len(protected) != 0 { + t.Fatalf("expected no protected peers with empty stats, got %d", len(protected)) + } +} + +func TestProtectedPeersTopPeer(t *testing.T) { + // 20 peers, 10% of 20 = 2 protected per category. + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + + peers := makePeers(20) + stats := make(map[string]txtracker.PeerStats) + stats[peers[0].ID().String()] = txtracker.PeerStats{RecentFinalized: 100} + stats[peers[1].ID().String()] = txtracker.PeerStats{RecentIncluded: 5.0} + + cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats } + + protected := cm.protectedPeers(peers) + if len(protected) != 2 { + t.Fatalf("expected 2 protected peers, got %d", len(protected)) + } + if !protected[peers[0]] { + t.Fatal("peer 0 should be protected (top RecentFinalized)") + } + if !protected[peers[1]] { + t.Fatal("peer 1 should be protected (top RecentIncluded)") + } +} + +func TestProtectedPeersZeroScore(t *testing.T) { + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + + peers := makePeers(10) + stats := make(map[string]txtracker.PeerStats) + for _, p := range peers { + stats[p.ID().String()] = txtracker.PeerStats{} + } + cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats } + + protected := cm.protectedPeers(peers) + if len(protected) != 0 { + t.Fatalf("expected no protection with zero scores, got %d", len(protected)) + } +} + +func TestProtectedPeersOverlap(t *testing.T) { + // One peer is top in both categories — counted once. + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + + peers := makePeers(20) + stats := make(map[string]txtracker.PeerStats) + stats[peers[0].ID().String()] = txtracker.PeerStats{RecentFinalized: 100, RecentIncluded: 5.0} + + cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats } + + protected := cm.protectedPeers(peers) + if len(protected) != 1 { + t.Fatalf("expected 1 protected peer (overlap), got %d", len(protected)) + } +} + +func TestProtectedPeersNilFunc(t *testing.T) { + cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} + // peerStatsFunc is nil (default). + + peers := makePeers(10) + protected := cm.protectedPeers(peers) + if protected != nil { + t.Fatalf("expected nil with nil stats func, got %v", protected) + } +} + +// TestProtectedByPoolPerPoolTopN verifies that the top-N selection runs +// independently in each of the inbound and dialed pools, not globally. +// With 10 peers per pool and inclusionProtectionFrac=0.1, exactly 1 peer +// is protected per pool per category — so 2 total (one per pool), both +// for the RecentFinalized category since we don't set RecentIncluded. +func TestProtectedByPoolPerPoolTopN(t *testing.T) { + inbound := makePeers(10) + dialed := makePeers(10) + // Distinguish dialed peer IDs from inbound so stats maps don't collide. + for i := range dialed { + id := enode.ID{byte(100 + i)} + dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil) + } + // Strictly increasing scores: highest wins in each pool. + stats := make(map[string]txtracker.PeerStats) + for i, p := range inbound { + stats[p.ID().String()] = txtracker.PeerStats{RecentFinalized: float64(1 + i)} + } + for i, p := range dialed { + stats[p.ID().String()] = txtracker.PeerStats{RecentFinalized: float64(1 + i)} + } + + protected := protectedPeersByPool(inbound, dialed, stats) + + // Expect top 1 of inbound (inbound[9]) and top 1 of dialed (dialed[9]). + if len(protected) != 2 { + t.Fatalf("expected 2 protected peers (1 per pool), got %d", len(protected)) + } + if !protected[inbound[9]] { + t.Error("expected top inbound peer to be protected") + } + if !protected[dialed[9]] { + t.Error("expected top dialed peer to be protected") + } +} + +// TestProtectedByPoolCrossCategoryOverlap verifies that the union across +// protection categories is correctly deduplicated: a peer that wins in +// multiple categories appears once, and category winners are all +// protected. Uses a pool large enough that frac*len yields n=2 per +// category, so cross-category overlap is observable. +func TestProtectedByPoolCrossCategoryOverlap(t *testing.T) { + // 20 dialed peers so 0.1 * 20 = 2 protected per category. + dialed := makePeers(20) + // P0: high RecentFinalized only. P1: high RecentIncluded only. P2: high both. + // With n=2 per category: + // RecentFinalized winners: P2 (tie-broken-ok), P0 + // RecentIncluded winners: P2, P1 + // Union: {P0, P1, P2}. + stats := make(map[string]txtracker.PeerStats) + stats[dialed[0].ID().String()] = txtracker.PeerStats{RecentFinalized: 100, RecentIncluded: 0} + stats[dialed[1].ID().String()] = txtracker.PeerStats{RecentFinalized: 0, RecentIncluded: 5.0} + stats[dialed[2].ID().String()] = txtracker.PeerStats{RecentFinalized: 200, RecentIncluded: 10.0} + + protected := protectedPeersByPool(nil, dialed, stats) + + if len(protected) != 3 { + t.Fatalf("expected 3 protected peers (union of category winners), got %d", len(protected)) + } + for _, idx := range []int{0, 1, 2} { + if !protected[dialed[idx]] { + t.Errorf("peer %d should be protected", idx) + } + } +} + +// TestProtectedByPoolPerPoolIndependence locks in that selection runs +// per-pool, not globally. Every inbound peer scores higher than every +// dialed peer, so a global top-N would pick only inbound peers. Per-pool +// top-N must still protect the top dialed peers. +func TestProtectedByPoolPerPoolIndependence(t *testing.T) { + // 20 inbound, 20 dialed — frac=0.1 → 2 protected per pool per category. + // Global top-4 of RecentFinalized would be inbound[16..19] — zero dialed. + inbound := makePeers(20) + dialed := make([]*p2p.Peer, 20) + for i := range dialed { + id := enode.ID{byte(100 + i)} + dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil) + } + stats := make(map[string]txtracker.PeerStats) + // Every inbound peer outscores every dialed peer. + for i, p := range inbound { + stats[p.ID().String()] = txtracker.PeerStats{RecentFinalized: float64(1000 + i)} + } + for i, p := range dialed { + stats[p.ID().String()] = txtracker.PeerStats{RecentFinalized: float64(1 + i)} + } + + protected := protectedPeersByPool(inbound, dialed, stats) + + // Per-pool top-2 of RecentFinalized: + // inbound: inbound[18], inbound[19] + // dialed: dialed[18], dialed[19] + // Global top-N would contain zero dialed peers, so asserting the top + // dialed peers are protected enforces per-pool independence. + if !protected[dialed[19]] { + t.Fatal("top dialed peer must be protected regardless of globally-higher inbound peers") + } + if !protected[dialed[18]] { + t.Fatal("second-top dialed peer must be protected regardless of globally-higher inbound peers") + } + if !protected[inbound[19]] || !protected[inbound[18]] { + t.Fatal("top inbound peers must also be protected") + } + if len(protected) != 4 { + t.Fatalf("expected 4 protected peers (top-2 of each pool), got %d", len(protected)) + } +} diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go index 20621c531d..8b917413ec 100644 --- a/eth/fetcher/tx_fetcher.go +++ b/eth/fetcher/tx_fetcher.go @@ -180,10 +180,11 @@ type TxFetcher struct { alternates map[common.Hash]map[string]struct{} // In-flight transaction alternate origins if retrieval fails // Callbacks - validateMeta func(common.Hash, byte) error // Validate a tx metadata based on the local txpool - addTxs func([]*types.Transaction) []error // Insert a batch of transactions into local txpool - fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer - dropPeer func(string) // Drops a peer in case of announcement violation + validateMeta func(common.Hash, byte) error // Validate a tx metadata based on the local txpool + addTxs func([]*types.Transaction) []error // Insert a batch of transactions into local txpool + fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer + dropPeer func(string) // Drops a peer in case of announcement violation + onAccepted func(peer string, hashes []common.Hash) // Optional: notified with accepted tx hashes per peer step chan struct{} // Notification channel when the fetcher loop iterates clock mclock.Clock // Monotonic clock or simulated clock for tests @@ -194,15 +195,15 @@ type TxFetcher struct { // NewTxFetcher creates a transaction fetcher to retrieve transaction // based on hash announcements. // Chain can be nil to disable on-chain checks. -func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher { - return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil) +func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), onAccepted func(string, []common.Hash)) *TxFetcher { + return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, onAccepted, mclock.System{}, time.Now, nil) } // NewTxFetcherForTests is a testing method to mock out the realtime clock with // a simulated version and the internal randomness with a deterministic one. // Chain can be nil to disable on-chain checks. func NewTxFetcherForTests( - chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), + chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), onAccepted func(string, []common.Hash), clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher { return &TxFetcher{ notify: make(chan *txAnnounce), @@ -224,6 +225,7 @@ func NewTxFetcherForTests( addTxs: addTxs, fetchTxs: fetchTxs, dropPeer: dropPeer, + onAccepted: onAccepted, clock: clock, realTime: realTime, rand: rand, @@ -344,6 +346,8 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) ) batch := txs[i:end] + var accepted []common.Hash + for j, err := range f.addTxs(batch) { // Track the transaction hash if the price is too low for us. // Avoid re-request this transaction when we receive another @@ -353,7 +357,8 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) } // Track a few interesting failure types switch { - case err == nil: // Noop, but need to handle to not count these + case err == nil: + accepted = append(accepted, batch[j].Hash()) case errors.Is(err, txpool.ErrAlreadyKnown): duplicate++ @@ -385,6 +390,10 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) underpricedMeter.Mark(underpriced) otherRejectMeter.Mark(otherreject) + // Notify the tracker which txs from this peer were accepted. + if f.onAccepted != nil && len(accepted) > 0 { + f.onAccepted(peer, accepted) + } // If 'other reject' is >25% of the deliveries in any batch, sleep a bit. if otherreject > int64((len(batch)+3)/4) { log.Debug("Peer delivering stale or invalid transactions", "peer", peer, "rejected", otherreject) diff --git a/eth/fetcher/tx_fetcher_test.go b/eth/fetcher/tx_fetcher_test.go index 6c2719631e..3fe11fda21 100644 --- a/eth/fetcher/tx_fetcher_test.go +++ b/eth/fetcher/tx_fetcher_test.go @@ -97,7 +97,7 @@ func newTestTxFetcher() *TxFetcher { return make([]error, len(txs)) }, func(string, []common.Hash) error { return nil }, - nil, + nil, nil, ) } @@ -2203,6 +2203,7 @@ func TestTransactionForgotten(t *testing.T) { }, func(string, []common.Hash) error { return nil }, func(string) {}, + nil, mockClock, mockTime, rand.New(rand.NewSource(0)), // Use fixed seed for deterministic behavior diff --git a/eth/handler.go b/eth/handler.go index 2028517cec..4a9573efbe 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/eth/fetcher" "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/snap" + "github.com/ethereum/go-ethereum/eth/txtracker" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" @@ -123,6 +124,7 @@ type handler struct { downloader *downloader.Downloader txFetcher *fetcher.TxFetcher + txTracker *txtracker.Tracker peers *peerSet txBroadcastKey [16]byte @@ -182,7 +184,8 @@ func newHandler(config *handlerConfig) (*handler, error) { } return nil } - h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer) + h.txTracker = txtracker.New() + h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer, h.txTracker.NotifyAccepted) return h, nil } @@ -403,6 +406,7 @@ func (h *handler) unregisterPeer(id string) { } h.downloader.UnregisterPeer(id) h.txFetcher.Drop(id) + h.txTracker.NotifyPeerDrop(id) if err := h.peers.unregisterPeer(id); err != nil { logger.Error("Ethereum peer removal failed", "err", err) @@ -426,6 +430,9 @@ func (h *handler) Start(maxPeers int) { // start sync handlers h.txFetcher.Start() + // Start the transaction tracker (records tx deliveries, credits peer inclusions). + h.txTracker.Start(h.chain) + // start peer handler tracker h.wg.Add(1) go h.protoTracker() diff --git a/eth/txtracker/tracker.go b/eth/txtracker/tracker.go new file mode 100644 index 0000000000..3ddd0231a8 --- /dev/null +++ b/eth/txtracker/tracker.go @@ -0,0 +1,320 @@ +// 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 txtracker provides minimal per-peer transaction inclusion tracking. +// +// It records which peer delivered each accepted transaction (via NotifyAccepted) +// and monitors the chain for inclusion and finalization events. When a +// delivered transaction is finalized on chain, the delivering peer is +// credited. A per-block exponential moving average (EMA) of inclusions +// tracks recent peer productivity. +// +// The primary consumer is the peer dropper (eth/dropper.go), which uses +// these stats to protect high-value peers from random disconnection. +package txtracker + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" +) + +const ( + // Maximum number of tx→deliverer mappings to retain. + maxTracked = 262144 + // EMA smoothing factor for per-block inclusion rate. + emaAlpha = 0.05 + // EMA smoothing factor for per-block finalization rate. Very slow on + // purpose: finalization is permanent, and the score should reflect + // sustained contribution over long windows, not recent bursts. + // Half-life ≈ 6930 chain heads (~23 hours on 12s blocks). + finalizedEMAAlpha = 0.0001 +) + +// PeerStats holds the per-peer inclusion data. +type PeerStats struct { + RecentFinalized float64 // EMA of per-block finalization credits (slow) + RecentIncluded float64 // EMA of per-block inclusions (fast) +} + +// Chain is the blockchain interface needed by the tracker. +type Chain interface { + SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription + GetBlock(hash common.Hash, number uint64) *types.Block + GetCanonicalHash(number uint64) common.Hash + CurrentFinalBlock() *types.Header +} + +type peerStats struct { + recentFinalized float64 + recentIncluded float64 +} + +// TxInfo records the per-transaction state the tracker maintains. +// +// Deliverer is the peer that first handed us this tx via NotifyAccepted. +// AddedAt is the unix-seconds wall-clock at that moment; it is compared +// against block.Time() to suppress credit for txs delivered at or after +// the slot of their inclusion block (re-broadcasts of just-mined txs). +// +// BlockNum / BlockHash are populated when the tracker first sees the tx +// in a head block (BlockNum == 0 means not yet seen on chain). BlockHash +// is re-checked against canonical-at-height at finalization time so +// reorgs do not yield credit. +type TxInfo struct { + Deliverer string + AddedAt uint64 + BlockNum uint64 + BlockHash common.Hash +} + +// Tracker records which peer delivered each transaction and credits peers +// when their transactions appear on chain. +type Tracker struct { + mu sync.Mutex + txs lru.BasicLRU[common.Hash, *TxInfo] // tx hash -> tx info with lru eviction + peers map[string]*peerStats + + chain Chain + lastFinalNum uint64 // last finalized block number processed + headCh chan core.ChainHeadEvent + sub event.Subscription + + quit chan struct{} + stopOnce sync.Once + step chan struct{} // test sync: sent after each event is processed + now func() uint64 // unix-seconds clock; overridable in tests + wg sync.WaitGroup +} + +// New creates a new tracker. +func New() *Tracker { + return &Tracker{ + txs: lru.NewBasicLRU[common.Hash, *TxInfo](maxTracked), + peers: make(map[string]*peerStats), + quit: make(chan struct{}), + step: make(chan struct{}, 1), + now: func() uint64 { return uint64(time.Now().Unix()) }, + } +} + +// Start begins listening for chain head events. +func (t *Tracker) Start(chain Chain) { + t.chain = chain + // Seed lastFinalNum so checkFinalization doesn't backfill from genesis. + if fh := chain.CurrentFinalBlock(); fh != nil { + t.lastFinalNum = fh.Number.Uint64() + } + t.headCh = make(chan core.ChainHeadEvent, 128) + t.sub = chain.SubscribeChainHeadEvent(t.headCh) + t.wg.Add(1) + go t.loop() +} + +// NotifyPeerDrop removes a disconnected peer's stats to prevent unbounded +// growth. Safe to call from any goroutine. +func (t *Tracker) NotifyPeerDrop(peer string) { + t.mu.Lock() + defer t.mu.Unlock() + delete(t.peers, peer) +} + +// Stop shuts down the tracker. +func (t *Tracker) Stop() { + t.stopOnce.Do(func() { + if t.sub != nil { + t.sub.Unsubscribe() + } + close(t.quit) + }) + t.wg.Wait() +} + +// NotifyAccepted records that a peer delivered transactions that were accepted +// by the pool. Only accepted (not rejected/duplicate) txs should be recorded +// to prevent attribution poisoning from replayed or invalid txs. +// Safe to call from any goroutine. +func (t *Tracker) NotifyAccepted(peer string, hashes []common.Hash) { + t.mu.Lock() + defer t.mu.Unlock() + + addedAt := t.now() + for _, hash := range hashes { + if t.txs.Contains(hash) { + continue // already tracked, keep first deliverer + } + t.txs.Add(hash, &TxInfo{Deliverer: peer, AddedAt: addedAt}) + } + // Ensure the delivering peer has a stats entry. + if len(hashes) > 0 && t.peers[peer] == nil { + t.peers[peer] = &peerStats{} + } +} + +// GetAllPeerStats returns a snapshot of per-peer inclusion statistics. +// Safe to call from any goroutine. +func (t *Tracker) GetAllPeerStats() map[string]PeerStats { + t.mu.Lock() + defer t.mu.Unlock() + + result := make(map[string]PeerStats, len(t.peers)) + for id, ps := range t.peers { + result[id] = PeerStats{ + RecentFinalized: ps.recentFinalized, + RecentIncluded: ps.recentIncluded, + } + } + return result +} + +func (t *Tracker) loop() { + defer t.wg.Done() + + for { + select { + case ev := <-t.headCh: + t.handleChainHead(ev) + select { + case t.step <- struct{}{}: + default: + } + case <-t.sub.Err(): + return + case <-t.quit: + return + } + } +} + +func (t *Tracker) handleChainHead(ev core.ChainHeadEvent) { + // Fetch the head block by hash (not just number) to avoid using a + // reorged block if the tracker goroutine lags behind the chain. + block := t.chain.GetBlock(ev.Header.Hash(), ev.Header.Number.Uint64()) + if block == nil { + return + } + t.mu.Lock() + defer t.mu.Unlock() + + // Count per-peer inclusions in this block for the inclusion EMA, and + // record (BlockNum, BlockHash) on first inclusion so the iterate-t.txs + // finalization scan can find the entry later without re-reading the + // block. Skip txs whose delivery arrived at or after this block's slot + // — those are likely post-slot re-broadcasts of an already-mined tx, + // not genuine relay work. + blockTime := block.Time() + blockNum := block.Number().Uint64() + blockHash := block.Hash() + blockIncl := make(map[string]int) + for _, tx := range block.Transactions() { + ti, ok := t.txs.Peek(tx.Hash()) + if !ok || ti.AddedAt >= blockTime { + continue + } + blockIncl[ti.Deliverer]++ + if ti.BlockNum == 0 { + ti.BlockNum = blockNum + ti.BlockHash = blockHash + } + } + // Accumulate per-peer finalization credits over the newly-finalized + // range (possibly zero blocks). Only counts peers still tracked. + blockFinal := t.collectFinalizationCredits() + + // Update both EMAs for all tracked peers (decays inactive ones). + // Don't create entries for unknown peers — they may have been + // removed by NotifyPeerDrop and should not be resurrected. + for peer, ps := range t.peers { + ps.recentIncluded = (1-emaAlpha)*ps.recentIncluded + emaAlpha*float64(blockIncl[peer]) + ps.recentFinalized = (1-finalizedEMAAlpha)*ps.recentFinalized + finalizedEMAAlpha*float64(blockFinal[peer]) + } +} + +// collectFinalizationCredits accumulates per-peer finalization credits for +// blocks newly finalized since lastFinalNum, and advances lastFinalNum. +// Returns a (possibly empty) credits map keyed by peer ID. Must be called +// with t.mu held. +// +// The pivot here is to iterate t.txs (which we already maintain by hash +// with BlockNum + BlockHash recorded at inclusion time) rather than +// walking each newly-finalized block from disk. The walk over chain +// blocks was the dominant cost during catch-up after a restart: every +// block called GetBlockByNumber (cold-disk RLP-decode) and then per-tx +// tx.Hash() and types.Sender() against fresh cache-cold *Transaction +// instances. By inverting, the only chain query is one cheap canonical- +// hash lookup per unique BlockNum that has tracked entries, used to +// confirm the recorded BlockHash is still on the canonical chain (and +// thus the tx really is finalized). No tx iteration, no hashing, no +// sender derivation against cold blocks. +func (t *Tracker) collectFinalizationCredits() map[string]int { + credits := make(map[string]int) + finalHeader := t.chain.CurrentFinalBlock() + if finalHeader == nil { + return credits + } + finalNum := finalHeader.Number.Uint64() + if finalNum <= t.lastFinalNum { + return credits + } + + // Group entries by their recorded BlockNum so the canonical-hash + // lookup happens once per height, not once per tx. The BlockNum range + // check filters both "not yet seen on chain" (BlockNum == 0) and + // "already credited in a prior pass" (BlockNum <= lastFinalNum); no + // separate status bookkeeping is needed. + buckets := make(map[uint64][]*TxInfo) + for _, hash := range t.txs.Keys() { + ti, ok := t.txs.Peek(hash) + if !ok || ti.BlockNum <= t.lastFinalNum || ti.BlockNum > finalNum { + continue + } + buckets[ti.BlockNum] = append(buckets[ti.BlockNum], ti) + } + + total := 0 + for num, tis := range buckets { + canonHash := t.chain.GetCanonicalHash(num) + if canonHash == (common.Hash{}) { + continue + } + for _, ti := range tis { + // BlockHash was recorded when the entry was first seen + // on chain. If it doesn't match the canonical hash now, + // the entry's recorded inclusion is in an orphaned + // block; skip rather than misreport finality. + if ti.BlockHash != canonHash { + continue + } + if ti.Deliverer != "" { + credits[ti.Deliverer]++ + total++ + } + } + } + + if total > 0 { + log.Trace("Accumulated finalization credits", + "from", t.lastFinalNum+1, "to", finalNum, "txs", total) + } + t.lastFinalNum = finalNum + return credits +} diff --git a/eth/txtracker/tracker_test.go b/eth/txtracker/tracker_test.go new file mode 100644 index 0000000000..8e7e81d034 --- /dev/null +++ b/eth/txtracker/tracker_test.go @@ -0,0 +1,501 @@ +// 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 txtracker + +import ( + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/trie" +) + +// mockChain implements the Chain interface for testing. +// +// Blocks are stored by hash to exercise the reorg-safe lookup path in +// tracker.handleChainHead (which calls GetBlock(hash, number)). A separate +// canonicalByNum index maps each height to its canonical block hash, used +// by GetCanonicalHash in the finalization-credit path. +type mockChain struct { + mu sync.Mutex + headFeed event.Feed + blocksByHash map[common.Hash]*types.Block + canonicalByNum map[uint64]common.Hash + finalNum uint64 +} + +func newMockChain() *mockChain { + return &mockChain{ + blocksByHash: make(map[common.Hash]*types.Block), + canonicalByNum: make(map[uint64]common.Hash), + } +} + +func (c *mockChain) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + return c.headFeed.Subscribe(ch) +} + +func (c *mockChain) GetBlock(hash common.Hash, number uint64) *types.Block { + c.mu.Lock() + defer c.mu.Unlock() + return c.blocksByHash[hash] +} + +func (c *mockChain) GetCanonicalHash(number uint64) common.Hash { + c.mu.Lock() + defer c.mu.Unlock() + return c.canonicalByNum[number] +} + +func (c *mockChain) CurrentFinalBlock() *types.Header { + c.mu.Lock() + defer c.mu.Unlock() + if c.finalNum == 0 { + return nil + } + return &types.Header{Number: new(big.Int).SetUint64(c.finalNum)} +} + +// addBlock adds a canonical block at the given height. Overwrites any +// prior canonical block at that height. +func (c *mockChain) addBlock(num uint64, txs []*types.Transaction) *types.Block { + return c.addBlockAtHeight(num, num, txs, true) +} + +// addBlockAtHeight adds a block at the given height. The salt parameter +// ensures distinct block hashes for two blocks at the same height (used +// for reorg tests). If canonical is true, the block becomes the canonical +// block for that height (looked up by GetCanonicalHash). +func (c *mockChain) addBlockAtHeight(num, salt uint64, txs []*types.Transaction, canonical bool) *types.Block { + return c.addBlockAtHeightWithTime(num, salt, txs, canonical, uint64(time.Now().Unix()+3600)) +} + +// addBlockAtHeightWithTime is like addBlockAtHeight but takes an explicit +// block time. Used by the pre-slot gate test, which needs a block whose +// slot start is BEFORE the moment NotifyAccepted recorded its tx. +func (c *mockChain) addBlockAtHeightWithTime(num, salt uint64, txs []*types.Transaction, canonical bool, blockTime uint64) *types.Block { + c.mu.Lock() + defer c.mu.Unlock() + // Mix salt into Extra so siblings at the same height get distinct hashes. + header := &types.Header{ + Number: new(big.Int).SetUint64(num), + Extra: big.NewInt(int64(salt)).Bytes(), + Time: blockTime, + } + block := types.NewBlock(header, &types.Body{Transactions: txs}, nil, trie.NewListHasher()) + c.blocksByHash[block.Hash()] = block + if canonical { + c.canonicalByNum[num] = block.Hash() + } + return block +} + +func (c *mockChain) setFinalBlock(num uint64) { + c.mu.Lock() + defer c.mu.Unlock() + c.finalNum = num +} + +// sendHead emits a chain head event for the canonical block at the given +// height. The emitted header carries the real block's hash so the +// tracker's GetBlock(hash, number) lookup resolves correctly. +func (c *mockChain) sendHead(num uint64) { + c.mu.Lock() + hash := c.canonicalByNum[num] + block := c.blocksByHash[hash] + c.mu.Unlock() + if block == nil { + panic("sendHead: no canonical block at height") + } + c.headFeed.Send(core.ChainHeadEvent{Header: block.Header()}) +} + +// sendHeadBlock emits a chain head event for the given block (may be +// non-canonical). Used for reorg tests. +func (c *mockChain) sendHeadBlock(block *types.Block) { + c.headFeed.Send(core.ChainHeadEvent{Header: block.Header()}) +} + +func hashTxs(txs []*types.Transaction) []common.Hash { + hashes := make([]common.Hash, len(txs)) + for i, tx := range txs { + hashes[i] = tx.Hash() + } + return hashes +} + +func makeTx(nonce uint64) *types.Transaction { + return types.NewTx(&types.LegacyTx{Nonce: nonce, GasPrice: big.NewInt(1), Gas: 21000}) +} + +// waitStep blocks until the tracker has processed one event. +func waitStep(t *testing.T, tr *Tracker) { + t.Helper() + select { + case <-tr.step: + case <-time.After(time.Second): + t.Fatal("timeout waiting for tracker step") + } +} + +func TestNotifyReceived(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + txs := []*types.Transaction{makeTx(1), makeTx(2), makeTx(3)} + hashes := hashTxs(txs) + tr.NotifyAccepted("peerA", hashes) + + // Public surface: peer entry was created with zero stats before any + // chain events. Map lookups would return a zero value for a missing + // key, so assert presence explicitly. + stats := tr.GetAllPeerStats() + if len(stats) != 1 { + t.Fatalf("expected 1 peer entry, got %d", len(stats)) + } + ps, ok := stats["peerA"] + if !ok { + t.Fatal("expected peerA entry, not found") + } + if ps.RecentFinalized != 0 || ps.RecentIncluded != 0 { + t.Fatalf("expected zero stats before chain events, got %+v", ps) + } + + // Internal state: all tx→deliverer mappings recorded. + tr.mu.Lock() + defer tr.mu.Unlock() + if tr.txs.Len() != 3 { + t.Fatalf("expected 3 tracked txs, got %d", tr.txs.Len()) + } + for i, h := range hashes { + got, ok := tr.txs.Peek(h) + if !ok { + t.Fatalf("tx %d: not tracked", i) + } + if got.Deliverer != "peerA" { + t.Fatalf("tx %d: expected deliverer=peerA, got %q", i, got.Deliverer) + } + } +} + +func TestInclusionEMA(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Block 1 includes peerA's tx. + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentIncluded <= 0 { + t.Fatalf("expected RecentIncluded > 0 after inclusion, got %f", stats["peerA"].RecentIncluded) + } + ema1 := stats["peerA"].RecentIncluded + + // Block 2 has no txs from peerA — EMA should decay. + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + stats = tr.GetAllPeerStats() + if stats["peerA"].RecentIncluded >= ema1 { + t.Fatalf("expected EMA to decay, got %f >= %f", stats["peerA"].RecentIncluded, ema1) + } +} + +func TestFinalization(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Include in block 1. + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + + // Not finalized yet. + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentFinalized != 0 { + t.Fatalf("expected RecentFinalized=0 before finalization, got %f", stats["peerA"].RecentFinalized) + } + + // Finalize block 1, then send head 2 to trigger the finalization EMA update. + chain.setFinalBlock(1) + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + stats = tr.GetAllPeerStats() + if stats["peerA"].RecentFinalized <= 0 { + t.Fatalf("expected RecentFinalized>0 after finalization, got %f", stats["peerA"].RecentFinalized) + } +} + +func TestMultiplePeers(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx1 := makeTx(1) + tx2 := makeTx(2) + tr.NotifyAccepted("peerA", []common.Hash{tx1.Hash()}) + tr.NotifyAccepted("peerB", []common.Hash{tx2.Hash()}) + + // Both included in block 1. + chain.addBlock(1, []*types.Transaction{tx1, tx2}) + chain.sendHead(1) + waitStep(t, tr) + + // Finalize. + chain.setFinalBlock(1) + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentFinalized <= 0 { + t.Fatalf("peerA: expected RecentFinalized>0, got %f", stats["peerA"].RecentFinalized) + } + if stats["peerB"].RecentFinalized <= 0 { + t.Fatalf("peerB: expected RecentFinalized>0, got %f", stats["peerB"].RecentFinalized) + } +} + +func TestFirstDelivererWins(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + tr.NotifyAccepted("peerB", []common.Hash{tx.Hash()}) // duplicate, should be ignored + + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + + chain.setFinalBlock(1) + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentFinalized <= 0 { + t.Fatalf("peerA should be credited, got RecentFinalized=%f", stats["peerA"].RecentFinalized) + } + if stats["peerB"].RecentFinalized != 0 { + t.Fatalf("peerB should NOT be credited, got RecentFinalized=%f", stats["peerB"].RecentFinalized) + } +} + +func TestNoFinalizationCredit(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Include but don't finalize. + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + + // Send more heads without finalization. + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentFinalized != 0 { + t.Fatalf("expected RecentFinalized=0 without finalization, got %f", stats["peerA"].RecentFinalized) + } +} + +func TestEMADecay(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Include in block 1. + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + + // Send 30 empty blocks — EMA should decay close to zero. + for i := uint64(2); i <= 31; i++ { + chain.addBlock(i, nil) + chain.sendHead(i) + waitStep(t, tr) + } + + stats := tr.GetAllPeerStats() + if stats["peerA"].RecentIncluded > 0.02 { + t.Fatalf("expected RecentIncluded near zero after 30 empty blocks, got %f", stats["peerA"].RecentIncluded) + } +} + +// TestReorgSafety verifies that handleChainHead resolves the head block by +// HASH (not just by number), so a head event announcing a sibling block at +// the same height does not credit transactions from the canonical block. +// +// Regression check: handleChainHead uses GetBlock(hash, number) so a head +// event announcing sibling B fetches B, not the canonical block A. +func TestReorgSafety(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Two blocks at height 1: canonical A contains tx; sibling B does not. + blockA := chain.addBlockAtHeight(1, 1, []*types.Transaction{tx}, true) + blockB := chain.addBlockAtHeight(1, 2, nil, false) + if blockA.Hash() == blockB.Hash() { + t.Fatal("sibling blocks ended up with the same hash") + } + + // Head announces sibling B. A hash-aware tracker fetches B, sees no + // peerA txs, and leaves the EMA at zero. A number-only tracker would + // instead fetch A and credit peerA. + chain.sendHeadBlock(blockB) + waitStep(t, tr) + + if got := tr.GetAllPeerStats()["peerA"].RecentIncluded; got != 0 { + t.Fatalf("expected RecentIncluded=0 after sibling-B head event, got %f (tracker followed the wrong block)", got) + } + + // Now announce canonical A; peerA should be credited. + chain.sendHeadBlock(blockA) + waitStep(t, tr) + + if got := tr.GetAllPeerStats()["peerA"].RecentIncluded; got <= 0 { + t.Fatalf("expected RecentIncluded>0 after canonical-A head event, got %f", got) + } +} + +// TestRecentFinalizedDecays verifies that the finalization EMA decays +// for a peer that earned credits in the past but has no new +// finalization activity. The decay is slow (α=0.0001), so we +// just assert monotonic decrease, not convergence to zero. +func TestRecentFinalizedDecays(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + tx := makeTx(1) + tr.NotifyAccepted("peerA", []common.Hash{tx.Hash()}) + + // Include and finalize in block 1. + chain.addBlock(1, []*types.Transaction{tx}) + chain.sendHead(1) + waitStep(t, tr) + chain.setFinalBlock(1) + chain.addBlock(2, nil) + chain.sendHead(2) + waitStep(t, tr) + + peak := tr.GetAllPeerStats()["peerA"].RecentFinalized + if peak <= 0 { + t.Fatalf("expected RecentFinalized>0 after finalization, got %f", peak) + } + + // Send many empty heads — peer contributes zero each block, + // EMA should decay monotonically. + for i := uint64(3); i <= 50; i++ { + chain.addBlock(i, nil) + chain.sendHead(i) + waitStep(t, tr) + } + + after := tr.GetAllPeerStats()["peerA"].RecentFinalized + if after >= peak { + t.Fatalf("expected RecentFinalized to decay, got %f >= peak %f", after, peak) + } +} + +// TestPreSlotGate verifies that a tx delivered at or after the slot of its +// inclusion block earns no credit. This blocks the simple +// post-block-propagation re-broadcast attribution attack: a peer that +// learns a tx from the just-mined block and re-broadcasts it to our pool +// should not gain credit when that block is processed. The finalization +// path applies the same gate (ti.AddedAt >= blockTime) and is exercised +// by the existing TestFinalization with the new clock semantics. +func TestPreSlotGate(t *testing.T) { + tr := New() + chain := newMockChain() + tr.Start(chain) + defer tr.Stop() + + // Pin the tracker's clock so NotifyAccepted records a known addedAt. + const delivery = uint64(1_000_000) + tr.now = func() uint64 { return delivery } + + // peerA delivers two txs at the same instant. + preTx := makeTx(1) + postTx := makeTx(2) + tr.NotifyAccepted("peerA", []common.Hash{preTx.Hash(), postTx.Hash()}) + + // Block 1: slot strictly AFTER delivery — pre-slot, credit allowed. + chain.addBlockAtHeightWithTime(1, 1, []*types.Transaction{preTx}, true, delivery+100) + chain.sendHead(1) + waitStep(t, tr) + + preEMA := tr.GetAllPeerStats()["peerA"].RecentIncluded + if preEMA <= 0 { + t.Fatalf("expected RecentIncluded>0 after pre-slot delivery, got %f", preEMA) + } + + // Block 2: slot strictly BEFORE delivery — post-slot, must NOT credit. + chain.addBlockAtHeightWithTime(2, 2, []*types.Transaction{postTx}, true, delivery-1) + chain.sendHead(2) + waitStep(t, tr) + + // With the gate, only EMA decay occurs (no contribution this block). + // Without the gate, RecentIncluded would have ticked up again. + after := tr.GetAllPeerStats()["peerA"].RecentIncluded + if after >= preEMA { + t.Fatalf("expected EMA to decay (no credit for post-slot tx), got %f >= preEMA %f", after, preEMA) + } +} diff --git a/tests/fuzzers/txfetcher/txfetcher_fuzzer.go b/tests/fuzzers/txfetcher/txfetcher_fuzzer.go index bcceaff383..69674d0e62 100644 --- a/tests/fuzzers/txfetcher/txfetcher_fuzzer.go +++ b/tests/fuzzers/txfetcher/txfetcher_fuzzer.go @@ -84,7 +84,7 @@ func fuzz(input []byte) int { return make([]error, len(txs)) }, func(string, []common.Hash) error { return nil }, - nil, + nil, nil, clock, func() time.Time { nanoTime := int64(clock.Now()) From 3ab52d837d7baec73b53cdfbdb3bfb5fee6a81fe Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Fri, 10 Jul 2026 11:42:16 +0200 Subject: [PATCH 09/20] go.mod: update ckzg (#35336) Updates ckzg to the newest version --- cmd/keeper/go.mod | 2 +- cmd/keeper/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index b2caee8b63..27829894e3 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -17,7 +17,7 @@ require ( github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.8 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index 1906759f12..1f85402b70 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -50,8 +50,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58= +github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= diff --git a/go.mod b/go.mod index 50b5519762..a4abedb00b 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 - github.com/ethereum/c-kzg-4844/v2 v2.1.6 + github.com/ethereum/c-kzg-4844/v2 v2.1.8 github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a github.com/fatih/color v1.16.0 diff --git a/go.sum b/go.sum index 2150365e17..4687cbd6a3 100644 --- a/go.sum +++ b/go.sum @@ -127,8 +127,8 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58= +github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a h1:eIFUceK3U/z9UV0D/kAI6cxA27eH7MPqt2ks7fbzj/k= From 0e2cbebbd199491a3198a92038582897d6bb9d74 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 13 Jul 2026 03:11:12 +0200 Subject: [PATCH 10/20] params: update contract addresses (#35341) As per https://notes.ethereum.org/@ethpandaops/glamsterdam-devnet-7 --- params/protocol_params.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/params/protocol_params.go b/params/protocol_params.go index c4a74b794a..f6f71e0dc0 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -260,10 +260,10 @@ var ( ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") // EIP-8282 - Builder Execution Requests - BuilderDepositAddress = common.HexToAddress("0x0000884d2AA32eAa155F59A2f24eFa73D9008282") - BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd") - BuilderExitAddress = common.HexToAddress("0x000014574A74c805590AFF9499fc7A690f008282") - BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd") + BuilderDepositAddress = common.HexToAddress("0x0000BFF46984E3725691FA540A8C7589300D8282") + BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1461011c575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146102705760015460088111605257506058565b60089003015b601190600182026001905f5b5f821115607f57810190830284830290049160010191906064565b90939004925050503660b814609f57366102705734610270575f5260205ff35b8034106102705760383567ffffffffffffffff1680633b9aca001161027057633b9aca00029034031061027057600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b60035460025480820380604011610131575060405b5f5b8181146101d7578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c816001015353602001816003015481526020018160040154815260200190600501549052600101610133565b91018092146101e957906002556101f4565b90505f6002555f6003555b36610242575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023057600882820111610238575b50505f610264565b0160089003610264565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f60015560b8025ff35b5f5ffd") + BuilderExitAddress = common.HexToAddress("0x000064D678505AD48F8CCB093BC65613800E8282") + BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460e1575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101c65760015460028111605157506057565b60029003015b601190600182026001905f5b5f821115607e57810190830284830290049160010191906063565b909390049250505036603014609e57366101c657346101c6575f5260205ff35b34106101c657600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160f5575060105b5f5b81811461012d5782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160f7565b910180921461013f579060025561014a565b90505f6002555f6003555b36610198575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101865760028282011161018e575b50505f6101ba565b01600290036101ba565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f6001556044025ff35b5f5ffd") // EIP-7997 - Deterministic deployment factory (keyless CREATE2 factory) DeterministicFactoryAddress = common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C") From e7314c8a13dad73bc98e53d9a94e6f429a6e00dc Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 13 Jul 2026 03:38:54 +0200 Subject: [PATCH 11/20] core: charge floorDataGas if it exceeds regularGas (#35342) Implement EIP spec change https://github.com/ethereum/EIPs/pull/11908/changes --------- Co-authored-by: Gary Rong --- core/state_transition.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index a3a9efeaa4..4967675164 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -882,14 +882,14 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g // EIP-8037: // tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir // tx_state_gas = intrinsic_state_gas + tx_output.execution_state_gas_used - // tx_regular_gas = tx_gas_used_before_refund - tx_state_gas + // tx_regular_gas = max(tx_gas_used_before_refund - tx_state_gas, calldata_floor_gas_cost) gasLeft := st.gasRemaining.RegularGas + st.gasRemaining.StateGas gasUsedBeforeRefund := st.msg.GasLimit - gasLeft if gasUsedBeforeRefund < txStateGas { return 0, 0, fmt.Errorf("negative topmost frame regular gas usage, total: %d, state: %d", gasUsedBeforeRefund, txStateGas) } - txRegularGas := gasUsedBeforeRefund - txStateGas + txRegularGas := max(gasUsedBeforeRefund-txStateGas, floorDataGas) // EIP-3529: tx_gas_refund = min(tx_gas_used_before_refund/5, refund_counter). refund := st.calcRefund(gasUsedBeforeRefund) From b34a925e9e5819d345a938ee2779fea9d59ce482 Mon Sep 17 00:00:00 2001 From: Stefan <22667037+qu0b@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:55:39 +0200 Subject: [PATCH 12/20] beacon/engine, eth/catalyst: return block access lists in payload bodies v2 (#35347) --- beacon/engine/types.go | 6 ++++ eth/catalyst/api.go | 31 +++++++++++------ eth/catalyst/api_test.go | 74 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 980e8f9771..abe46bb351 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -415,6 +415,12 @@ type ExecutionPayloadBody struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` } +// ExecutionPayloadBodyV2 extends ExecutionPayloadBody with the block access list. +type ExecutionPayloadBodyV2 struct { + ExecutionPayloadBody + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` +} + // Client identifiers to support ClientVersionV1. const ( ClientCode = "GE" diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 4b0c3f6ea3..5119051806 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -1176,13 +1176,13 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin return bodies } -// GetPayloadBodiesByHashV2 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list +// GetPayloadBodiesByHashV2 implements engine_getPayloadBodiesByHashV2 which allows for retrieval of a list // of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBody { - bodies := make([]*engine.ExecutionPayloadBody, len(hashes)) +func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBodyV2 { + bodies := make([]*engine.ExecutionPayloadBodyV2, len(hashes)) for i, hash := range hashes { block := api.eth.BlockChain().GetBlockByHash(hash) - bodies[i] = getBody(block) + bodies[i] = getBodyV2(block) } return bodies } @@ -1190,16 +1190,16 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engin // GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range // of block bodies by the engine api. func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { - return api.getBodiesByRange(start, count) + return getBodiesByRange(api, start, count, getBody) } -// GetPayloadBodiesByRangeV2 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range +// GetPayloadBodiesByRangeV2 implements engine_getPayloadBodiesByRangeV2 which allows for retrieval of a range // of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { - return api.getBodiesByRange(start, count) +func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV2, error) { + return getBodiesByRange(api, start, count, getBodyV2) } -func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { +func getBodiesByRange[T any](api *ConsensusAPI, start, count hexutil.Uint64, getBody func(*types.Block) *T) ([]*T, error) { if start == 0 || count == 0 { return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) } @@ -1212,7 +1212,7 @@ func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engin if last > current { last = current } - bodies := make([]*engine.ExecutionPayloadBody, 0, uint64(count)) + bodies := make([]*T, 0, uint64(count)) for i := uint64(start); i <= last; i++ { block := api.eth.BlockChain().GetBlockByNumber(i) bodies = append(bodies, getBody(block)) @@ -1241,6 +1241,17 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBody { return &result } +func getBodyV2(block *types.Block) *engine.ExecutionPayloadBodyV2 { + body := getBody(block) + if body == nil { + return nil + } + return &engine.ExecutionPayloadBodyV2{ + ExecutionPayloadBody: *body, + BlockAccessList: block.AccessList(), + } +} + // convertRequests converts a hex requests slice to plain [][]byte. func convertRequests(hex []hexutil.Bytes) [][]byte { if hex == nil { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 849c123979..783dda59b0 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "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/kzg4844" "github.com/ethereum/go-ethereum/eth" @@ -1366,7 +1367,7 @@ func TestGetBlockBodiesByHash(t *testing.T) { for k, test := range tests { result := api.GetPayloadBodiesByHashV2(test.hashes) for i, r := range result { - if err := checkEqualBody(test.results[i], r); err != nil { + if err := checkEqualBodyV2(test.results[i], r); err != nil { t.Fatalf("test %v: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r) } } @@ -1444,7 +1445,7 @@ func TestGetBlockBodiesByRange(t *testing.T) { } if len(result) == len(test.results) { for i, r := range result { - if err := checkEqualBody(test.results[i], r); err != nil { + if err := checkEqualBodyV2(test.results[i], r); err != nil { t.Fatalf("test %d: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r) } } @@ -1520,6 +1521,75 @@ func checkEqualBody(a *types.Body, b *engine.ExecutionPayloadBody) error { return nil } +func checkEqualBodyV2(a *types.Body, b *engine.ExecutionPayloadBodyV2) error { + if b == nil { + return checkEqualBody(a, nil) + } + return checkEqualBody(a, &b.ExecutionPayloadBody) +} + +func TestGetPayloadBodyV2BlockAccessList(t *testing.T) { + empty := bal.BlockAccessList{} + emptyHash := empty.Hash() + tests := []struct { + name string + header *types.Header + accessList *bal.BlockAccessList + want string + }{ + { + name: "retained empty BAL", + header: &types.Header{BlockAccessListHash: &emptyHash}, + accessList: &empty, + want: "[]", + }, + { + name: "pruned BAL", + header: &types.Header{BlockAccessListHash: &emptyHash}, + want: "null", + }, + { + name: "pre-Amsterdam block", + header: new(types.Header), + want: "null", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + block := types.NewBlockWithHeader(test.header).WithAccessListUnsafe(test.accessList) + body := getBodyV2(block) + encoded, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + t.Fatal(err) + } + if got := string(fields["blockAccessList"]); got != test.want { + t.Fatalf("unexpected blockAccessList: got %s, want %s", got, test.want) + } + }) + } +} + +func TestGetPayloadBodyV1OmitsBlockAccessList(t *testing.T) { + empty := bal.BlockAccessList{} + emptyHash := empty.Hash() + block := types.NewBlockWithHeader(&types.Header{BlockAccessListHash: &emptyHash}).WithAccessListUnsafe(&empty) + encoded, err := json.Marshal(getBody(block)) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + t.Fatal(err) + } + if _, ok := fields["blockAccessList"]; ok { + t.Fatal("V1 payload body contains blockAccessList") + } +} + func TestBlockToPayloadWithBlobs(t *testing.T) { header := types.Header{} var txs []*types.Transaction From 13bcbf0eef6b94a0966017d85209e2f110404819 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 13 Jul 2026 15:56:00 +0800 Subject: [PATCH 13/20] core/rawdb: fix off-by-one in freezer init progress log (#35344) --- core/rawdb/chain_iterator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rawdb/chain_iterator.go b/core/rawdb/chain_iterator.go index afa1aa7a4c..aa1b0eb10e 100644 --- a/core/rawdb/chain_iterator.go +++ b/core/rawdb/chain_iterator.go @@ -70,7 +70,7 @@ func InitDatabaseFromFreezer(db ethdb.Database) { i += uint64(len(data)) // If we've spent too much time already, notify the user of what we're doing if time.Since(logged) > 8*time.Second { - log.Info("Initializing database from freezer", "total", frozen, "number", i, "hash", hash, "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Initializing database from freezer", "total", frozen, "number", i-1, "hash", hash, "elapsed", common.PrettyDuration(time.Since(start))) logged = time.Now() } } From df7b89603c79744d6ca201560c492c409be7dd89 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 13 Jul 2026 17:06:26 +0800 Subject: [PATCH 14/20] internal/testlog: clone attributes before appending (#35324) WithAttrs and terminalFormat appended to h.attrs directly, which can mutate the shared slice when it has spare capacity. Clone attrs first to avoid corrupting parent handler state. --- internal/testlog/testlog.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/testlog/testlog.go b/internal/testlog/testlog.go index 8a3ea85438..8993d073c7 100644 --- a/internal/testlog/testlog.go +++ b/internal/testlog/testlog.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "log/slog" + "slices" "sync" "github.com/ethereum/go-ethereum/log" @@ -76,7 +77,7 @@ func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler { copy(records[:], h.buf[:]) return &bufHandler{ buf: records, - attrs: append(h.attrs, attrs...), + attrs: append(slices.Clone(h.attrs), attrs...), level: h.level, } } @@ -186,7 +187,7 @@ func (h *bufHandler) terminalFormat(r slog.Record) string { return true }) - attrs = append(h.attrs, attrs...) + attrs = append(slices.Clone(h.attrs), attrs...) fmt.Fprintf(buf, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Message) if length := len(r.Message); length < 40 { From 3140668d496c77fe4f4b08b123919667b782b0ab Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 13 Jul 2026 18:13:15 +0800 Subject: [PATCH 15/20] miner: cap configured MaxBlobsPerBlock to protocol limit (#35295) ## Summary - Only apply user-configured `MaxBlobsPerBlock` when it is strictly below the protocol-defined maximum. - Prevents the miner from building blocks that exceed the consensus blob limit. ## Test plan - [x] `go test -short ./miner/` --- miner/worker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miner/worker.go b/miner/worker.go index 7f0e11f30a..cd53addf9b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -51,8 +51,8 @@ var ( // Users can specify the maximum number of blobs per block if necessary. func (miner *Miner) maxBlobsPerBlock(time uint64) int { maxBlobs := eip4844.MaxBlobsPerBlock(miner.chainConfig, time) - if miner.config.MaxBlobsPerBlock != 0 { - maxBlobs = miner.config.MaxBlobsPerBlock + if configured := miner.config.MaxBlobsPerBlock; configured != 0 && configured < maxBlobs { + maxBlobs = configured } return maxBlobs } From f9382c2d1b989b87cdc5ba5cd1315766f7650d60 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 13 Jul 2026 13:51:38 +0200 Subject: [PATCH 16/20] eth/tracers/logger: respect logging limit (#35349) Necessary for building fuzzers that don't take 10s of seconds for tracing a test --- eth/tracers/logger/logger.go | 18 ++++++++++++++++-- eth/tracers/logger/logger_json.go | 10 ++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 8e445818ef..963282ca66 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -51,6 +51,19 @@ type Config struct { Overrides *params.ChainConfig `json:"overrides,omitempty"` } +// countingWriter wraps an io.Writer and records how many bytes have been +// written through it. +type countingWriter struct { + w io.Writer + n int +} + +func (c *countingWriter) Write(p []byte) (int, error) { + n, err := c.w.Write(p) + c.n += n + return n, err +} + //go:generate go run github.com/fjl/gencodec -type StructLog -field-override structLogMarshaling -out gen_structlog.go // StructLog is emitted to the EVM each cycle and lists information about the @@ -227,7 +240,7 @@ type StructLogger struct { writer io.Writer // If set, the logger will stream instead of store logs logs []json.RawMessage // buffer of json-encoded logs - resultSize int + resultSize int // total bytes of trace output interrupt atomic.Bool // Atomic flag to signal execution interruption reason atomic.Pointer[error] // Reason for the interruption, populated by Stop @@ -237,7 +250,7 @@ type StructLogger struct { // NewStreamingStructLogger returns a new streaming logger. func NewStreamingStructLogger(cfg *Config, writer io.Writer) *StructLogger { l := NewStructLogger(cfg) - l.writer = writer + l.writer = &countingWriter{w: writer} return l } @@ -334,6 +347,7 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope return } log.Write(l.writer) + l.resultSize = l.writer.(*countingWriter).n } // OnExit is called a call frame finishes processing. diff --git a/eth/tracers/logger/logger_json.go b/eth/tracers/logger/logger_json.go index 52ac3945d4..530f21b534 100644 --- a/eth/tracers/logger/logger_json.go +++ b/eth/tracers/logger/logger_json.go @@ -59,12 +59,14 @@ type jsonLogger struct { cfg *Config env *tracing.VMContext hooks *tracing.Hooks + written *countingWriter } // NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects // into the provided stream. func NewJSONLogger(cfg *Config, writer io.Writer) *tracing.Hooks { - l := &jsonLogger{encoder: json.NewEncoder(writer), cfg: cfg} + cw := &countingWriter{w: writer} + l := &jsonLogger{encoder: json.NewEncoder(cw), cfg: cfg, written: cw} if l.cfg == nil { l.cfg = &Config{} } @@ -81,7 +83,8 @@ func NewJSONLogger(cfg *Config, writer io.Writer) *tracing.Hooks { // NewJSONLoggerWithCallFrames creates a new EVM tracer that prints execution steps as JSON objects // into the provided stream. It also includes call frames in the output. func NewJSONLoggerWithCallFrames(cfg *Config, writer io.Writer) *tracing.Hooks { - l := &jsonLogger{encoder: json.NewEncoder(writer), cfg: cfg} + cw := &countingWriter{w: writer} + l := &jsonLogger{encoder: json.NewEncoder(cw), cfg: cfg, written: cw} if l.cfg == nil { l.cfg = &Config{} } @@ -102,6 +105,9 @@ func (l *jsonLogger) OnFault(pc uint64, op byte, gas uint64, cost uint64, scope } func (l *jsonLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + if l.cfg.Limit != 0 && l.written.n > l.cfg.Limit { + return + } memory := scope.MemoryData() stack := scope.StackData() From 68f711b9defc53cb1b29cf0a1c65a32bbed10254 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 13 Jul 2026 15:31:10 +0200 Subject: [PATCH 17/20] beacon/engine: correct rlp encoding/decoding (#35348) Correctly passes BAL around in engine api --------- Co-authored-by: Gary Rong --- beacon/engine/ed_codec.go | 79 +++++++++++++------------ beacon/engine/types.go | 119 ++++++++++++++++++++++---------------- eth/catalyst/api.go | 2 + 3 files changed, 109 insertions(+), 91 deletions(-) diff --git a/beacon/engine/ed_codec.go b/beacon/engine/ed_codec.go index 02a1fd3805..4a6ae70b69 100644 --- a/beacon/engine/ed_codec.go +++ b/beacon/engine/ed_codec.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -18,25 +17,25 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -69,25 +68,25 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -165,7 +164,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { e.SlotNumber = (*uint64)(dec.SlotNumber) } if dec.BlockAccessList != nil { - e.BlockAccessList = dec.BlockAccessList + e.BlockAccessList = *dec.BlockAccessList } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index abe46bb351..9af2af31c6 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -17,6 +17,7 @@ package engine import ( + "bytes" "fmt" "math/big" "slices" @@ -25,7 +26,9 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "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/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) @@ -83,40 +86,41 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - SlotNumber *uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` + BlockAccessList []byte `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. type executableDataMarshaling struct { - Number hexutil.Uint64 - GasLimit hexutil.Uint64 - GasUsed hexutil.Uint64 - Timestamp hexutil.Uint64 - BaseFeePerGas *hexutil.Big - ExtraData hexutil.Bytes - LogsBloom hexutil.Bytes - Transactions []hexutil.Bytes - BlobGasUsed *hexutil.Uint64 - ExcessBlobGas *hexutil.Uint64 - SlotNumber *hexutil.Uint64 + Number hexutil.Uint64 + GasLimit hexutil.Uint64 + GasUsed hexutil.Uint64 + Timestamp hexutil.Uint64 + BaseFeePerGas *hexutil.Big + ExtraData hexutil.Bytes + LogsBloom hexutil.Bytes + Transactions []hexutil.Bytes + BlobGasUsed *hexutil.Uint64 + ExcessBlobGas *hexutil.Uint64 + SlotNumber *hexutil.Uint64 + BlockAccessList hexutil.Bytes } // StatelessPayloadStatusV1 is the result of a stateless payload execution. @@ -320,7 +324,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H // to be nil. var blockAccessListHash *common.Hash if data.BlockAccessList != nil { - hash := data.BlockAccessList.Hash() + hash := crypto.Keccak256Hash(data.BlockAccessList) blockAccessListHash = &hash } header := &types.Header{ @@ -347,32 +351,45 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H SlotNumber: data.SlotNumber, BlockAccessListHash: blockAccessListHash, } - return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil + body := types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals} + if data.BlockAccessList != nil { + var accessList bal.BlockAccessList + if err := rlp.DecodeBytes(data.BlockAccessList, &accessList); err != nil { + return nil, fmt.Errorf("failed to decode BAL: %w", err) + } + return types.NewBlockWithHeader(header).WithBody(body).WithAccessListUnsafe(&accessList), nil + } + return types.NewBlockWithHeader(header).WithBody(body), nil } // BlockToExecutableData constructs the ExecutableData structure by filling the // fields from the given block. It assumes the given block is post-merge block. func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope { data := &ExecutableData{ - BlockHash: block.Hash(), - ParentHash: block.ParentHash(), - FeeRecipient: block.Coinbase(), - StateRoot: block.Root(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - BaseFeePerGas: block.BaseFee(), - Timestamp: block.Time(), - ReceiptsRoot: block.ReceiptHash(), - LogsBloom: block.Bloom().Bytes(), - Transactions: encodeTransactions(block.Transactions()), - Random: block.MixDigest(), - ExtraData: block.Extra(), - Withdrawals: block.Withdrawals(), - BlobGasUsed: block.BlobGasUsed(), - ExcessBlobGas: block.ExcessBlobGas(), - SlotNumber: block.SlotNumber(), - BlockAccessList: block.AccessList(), + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + Withdrawals: block.Withdrawals(), + BlobGasUsed: block.BlobGasUsed(), + ExcessBlobGas: block.ExcessBlobGas(), + SlotNumber: block.SlotNumber(), + } + if al := block.AccessList(); al != nil { + var buf bytes.Buffer + if err := rlp.Encode(&buf, al); err == nil { + data.BlockAccessList = buf.Bytes() + } } // Add blobs. diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 5119051806..bc8c300418 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -806,6 +806,8 @@ func (api *ConsensusAPI) NewPayloadV5(ctx context.Context, params engine.Executa return invalidStatus, paramsErr("nil executionRequests post-prague") case params.SlotNumber == nil: return invalidStatus, paramsErr("nil slotnumber post-amsterdam") + case params.BlockAccessList == nil: + return invalidStatus, paramsErr("nil block access list post-amsterdam") case !api.checkFork(params.Timestamp, forks.Amsterdam): return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads") } From abfb2de5749ca059c368a1c000271378dc6b7bb5 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Tue, 14 Jul 2026 09:48:30 +0200 Subject: [PATCH 18/20] eth/catalyst, eth/ethconfig, cmd: make engine API max reorg depth configurable (#35335) --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 14 ++++++++++ eth/backend.go | 1 + eth/catalyst/api.go | 12 ++++---- eth/catalyst/api_test.go | 56 +++++++++++++++++++++++++++++++++++-- eth/ethconfig/config.go | 6 ++++ eth/ethconfig/gen_config.go | 6 ++++ 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 14e3a549cb..0108867f5a 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -174,6 +174,7 @@ var ( utils.RPCGlobalEVMTimeoutFlag, utils.RPCGlobalTxFeeCapFlag, utils.RPCGlobalLogQueryLimit, + utils.EngineMaxReorgDepthFlag, utils.AllowUnprotectedTxs, utils.BatchRequestLimit, utils.BatchResponseMaxSize, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index fd9893fe81..4082717930 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -679,6 +679,12 @@ var ( Value: ethconfig.Defaults.RangeLimit, Category: flags.APICategory, } + EngineMaxReorgDepthFlag = &cli.Uint64Flag{ + Name: "engine.maxreorgdepth", + Usage: "Maximum depth the chain head can be rewound to a canonical ancestor via engine forkchoiceUpdated (0 = no limit)", + Value: ethconfig.Defaults.EngineMaxReorgDepth, + Category: flags.APICategory, + } // Authenticated RPC HTTP settings AuthListenFlag = &cli.StringFlag{ Name: "authrpc.addr", @@ -1915,6 +1921,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(RPCGlobalTxFeeCapFlag.Name) { cfg.RPCTxFeeCap = ctx.Float64(RPCGlobalTxFeeCapFlag.Name) } + if ctx.IsSet(EngineMaxReorgDepthFlag.Name) { + cfg.EngineMaxReorgDepth = ctx.Uint64(EngineMaxReorgDepthFlag.Name) + } + if cfg.EngineMaxReorgDepth != 0 { + log.Info("Engine API maximum reorg depth", "depth", cfg.EngineMaxReorgDepth) + } else { + log.Info("Engine API reorg depth limit disabled") + } if ctx.Bool(NoDiscoverFlag.Name) { cfg.EthDiscoveryURLs, cfg.SnapDiscoveryURLs = []string{}, []string{} } else if ctx.IsSet(DNSDiscoveryFlag.Name) { diff --git a/eth/backend.go b/eth/backend.go index 72bda3191f..c880e75f27 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -449,6 +449,7 @@ func (s *Ethereum) Downloader() *downloader.Downloader { return s.handler.downlo func (s *Ethereum) Synced() bool { return s.handler.synced.Load() } func (s *Ethereum) SetSynced() { s.handler.enableSyncedFeatures() } func (s *Ethereum) ArchiveMode() bool { return s.config.NoPruning } +func (s *Ethereum) EngineMaxReorgDepth() uint64 { return s.config.EngineMaxReorgDepth } // Protocols returns all the currently configured // network protocols to start. diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index bc8c300418..d66f96148d 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -82,14 +82,15 @@ const ( // beaconUpdateWarnFrequency is the frequency at which to warn the user that // the beacon client is offline. beaconUpdateWarnFrequency = 5 * time.Minute - - // maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated. - maxReorgDepth = 32 ) type ConsensusAPI struct { eth *eth.Ethereum + // maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated + // (0 = no limit). Configured via ethconfig.Config.EngineMaxReorgDepth. + maxReorgDepth uint64 + remoteBlocks *headerQueue // Cache of remote payloads received localBlocks *payloadQueue // Cache of local payloads generated @@ -145,6 +146,7 @@ func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI { } api := &ConsensusAPI{ eth: eth, + maxReorgDepth: eth.EngineMaxReorgDepth(), remoteBlocks: newHeaderQueue(), localBlocks: newPayloadQueue(), invalidBlocksHits: make(map[common.Hash]int), @@ -330,9 +332,9 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo return valid(nil), nil } depth := api.eth.BlockChain().CurrentBlock().Number.Uint64() - block.NumberU64() - if depth >= maxReorgDepth { + if api.maxReorgDepth > 0 && depth >= api.maxReorgDepth { log.Warn("Refusing too deep reorg", "depth", depth, "head", update.HeadBlockHash) - return engine.STATUS_INVALID, engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds limit %d", depth, maxReorgDepth)) + return engine.STATUS_INVALID, engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds limit %d", depth, api.maxReorgDepth)) } if !api.eth.Synced() { log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash) diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 783dda59b0..2d3c4929ea 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -427,8 +427,9 @@ func TestEth2DeepReorg(t *testing.T) { */ } -// startEthService creates a full node instance for testing. -func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { +// startEthService creates a full node instance for testing. The default test +// configuration can be adjusted through optional modifier functions. +func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block, mods ...func(*ethconfig.Config)) (*node.Node, *eth.Ethereum) { t.Helper() n, err := node.New(&node.Config{ @@ -449,6 +450,9 @@ func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block) TrieCleanCache: 256, Miner: miner.DefaultConfig, } + for _, mod := range mods { + mod(ethcfg) + } ethservice, err := eth.New(n, ethcfg) if err != nil { t.Fatal("can't create eth service:", err) @@ -468,6 +472,54 @@ func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block) return n, ethservice } +// TestForkchoiceUpdatedReorgDepthLimit tests that forkchoiceUpdated refuses to +// rewind the chain head to a canonical ancestor deeper than the configured +// EngineMaxReorgDepth, and that the limit can be lifted via the configuration. +func TestForkchoiceUpdatedReorgDepthLimit(t *testing.T) { + genesis, blocks := generateMergeChain(10, true) + + t.Run("limited", func(t *testing.T) { + n, ethservice := startEthService(t, genesis, blocks, func(cfg *ethconfig.Config) { + cfg.EngineMaxReorgDepth = 5 + }) + defer n.Close() + + api := newConsensusAPIWithoutHeartbeat(ethservice) + + // Rewinding the head a few blocks within the limit is accepted. + shallow := engine.ForkchoiceStateV1{HeadBlockHash: blocks[6].Hash()} + if _, err := api.ForkchoiceUpdatedV1(context.Background(), shallow, nil); err != nil { + t.Fatalf("rewind within reorg depth limit failed: %v", err) + } + if head := ethservice.BlockChain().CurrentBlock().Number.Uint64(); head != blocks[6].NumberU64() { + t.Fatalf("chain head not rewound: have %d, want %d", head, blocks[6].NumberU64()) + } + // Rewinding beyond the limit is refused. + deep := engine.ForkchoiceStateV1{HeadBlockHash: genesis.ToBlock().Hash()} + _, err := api.ForkchoiceUpdatedV1(context.Background(), deep, nil) + var apiErr *engine.EngineAPIError + if !errors.As(err, &apiErr) || apiErr.ErrorCode() != engine.TooDeepReorg.ErrorCode() { + t.Fatalf("rewind beyond reorg depth limit: have error %v, want %v", err, engine.TooDeepReorg) + } + }) + t.Run("unlimited", func(t *testing.T) { + n, ethservice := startEthService(t, genesis, blocks, func(cfg *ethconfig.Config) { + cfg.EngineMaxReorgDepth = 0 // no limit + }) + defer n.Close() + + api := newConsensusAPIWithoutHeartbeat(ethservice) + + update := engine.ForkchoiceStateV1{HeadBlockHash: genesis.ToBlock().Hash()} + if _, err := api.ForkchoiceUpdatedV1(context.Background(), update, nil); err != nil { + t.Fatalf("rewind with disabled reorg depth limit failed: %v", err) + } + if head := ethservice.BlockChain().CurrentBlock().Number.Uint64(); head != 0 { + t.Fatalf("chain head not rewound to genesis: have %d, want 0", head) + } + }) +} + func TestFullAPI(t *testing.T) { genesis, preMergeBlocks := generateMergeChain(10, false) n, ethservice := startEthService(t, genesis, preMergeBlocks) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 378bb64a1e..c6f2fadde9 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -75,6 +75,7 @@ var Defaults = Config{ RPCEVMTimeout: 5 * time.Second, GPO: FullNodeGPO, RPCTxFeeCap: 1, // 1 ether + EngineMaxReorgDepth: 32, TxSyncDefaultTimeout: 20 * time.Second, TxSyncMaxTimeout: 1 * time.Minute, SlowBlockThreshold: -1, // Disabled by default; set via --debug.logslowblock flag @@ -203,6 +204,11 @@ type Config struct { // send-transaction variants. The unit is ether. RPCTxFeeCap float64 + // EngineMaxReorgDepth is the maximum depth the chain head can be rewound + // to an already-canonical ancestor by engine API forkchoiceUpdated calls + // (0 = no limit). + EngineMaxReorgDepth uint64 `toml:",omitempty"` + // OverrideOsaka (TODO: remove after the fork) OverrideOsaka *uint64 `toml:",omitempty"` diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index b24cde8a6a..167c93e685 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -63,6 +63,7 @@ func (c Config) MarshalTOML() (interface{}, error) { RPCGasCap uint64 RPCEVMTimeout time.Duration RPCTxFeeCap float64 + EngineMaxReorgDepth uint64 `toml:",omitempty"` OverrideOsaka *uint64 `toml:",omitempty"` OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` @@ -119,6 +120,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RPCGasCap = c.RPCGasCap enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCTxFeeCap = c.RPCTxFeeCap + enc.EngineMaxReorgDepth = c.EngineMaxReorgDepth enc.OverrideOsaka = c.OverrideOsaka enc.OverrideAmsterdam = c.OverrideAmsterdam enc.OverrideBPO1 = c.OverrideBPO1 @@ -179,6 +181,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RPCGasCap *uint64 RPCEVMTimeout *time.Duration RPCTxFeeCap *float64 + EngineMaxReorgDepth *uint64 `toml:",omitempty"` OverrideOsaka *uint64 `toml:",omitempty"` OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` @@ -330,6 +333,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.RPCTxFeeCap != nil { c.RPCTxFeeCap = *dec.RPCTxFeeCap } + if dec.EngineMaxReorgDepth != nil { + c.EngineMaxReorgDepth = *dec.EngineMaxReorgDepth + } if dec.OverrideOsaka != nil { c.OverrideOsaka = dec.OverrideOsaka } From 1ef0ffb98c19c9ebeecab23834764042678e5774 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 14 Jul 2026 20:28:26 +0800 Subject: [PATCH 19/20] core: implement EIP-2780 and EIP-8037 changes (#35318) Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - https://github.com/ethereum/EIPs/pull/11844 - https://github.com/ethereum/EIPs/pull/11891 - https://github.com/ethereum/EIPs/pull/11906 - https://github.com/ethereum/EIPs/commit/a4801f3bb1d1380ecc7db5f988b222684ae098eb --------- Co-authored-by: MariusVanDerWijden --- cmd/evm/internal/t8ntool/transaction.go | 8 +- core/bench_test.go | 4 +- core/bintrie_witness_test.go | 8 +- core/eip2780_test.go | 643 +++++++++++++++++- core/eip8037_test.go | 384 +++++++++-- core/eip8038_test.go | 117 ++-- core/error.go | 5 + core/state_transition.go | 574 +++++++++------- core/state_transition_test.go | 95 ++- .../tracing/gen_gas_change_reason_stringer.go | 8 +- core/tracing/hooks.go | 9 + core/txpool/validation.go | 10 +- core/vm/common.go | 1 - core/vm/evm.go | 82 ++- core/vm/gas_table.go | 56 +- core/vm/gascosts.go | 89 +-- core/vm/instructions.go | 30 +- core/vm/interface.go | 2 + core/vm/interpreter.go | 4 +- core/vm/operations_acl.go | 2 +- core/vm/runtime/runtime.go | 2 +- params/protocol_params.go | 25 +- tests/block_test.go | 1 - tests/gen_sttransaction.go | 8 +- tests/state_test_util.go | 16 +- tests/transaction_test_util.go | 6 +- 26 files changed, 1600 insertions(+), 589 deletions(-) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 7207cad41d..805b32ee02 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -140,15 +140,15 @@ func Transaction(ctx *cli.Context) error { value = uint256.NewInt(1) } rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), value, rules, params.CostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), value, rules) if err != nil { r.Error = err results = append(results, r) continue } - r.IntrinsicGas = cost.RegularGas - if tx.Gas() < cost.RegularGas { - r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), cost.RegularGas) + r.IntrinsicGas = cost + if tx.Gas() < cost { + r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), cost) results = append(results, r) continue } diff --git a/core/bench_test.go b/core/bench_test.go index 79584309da..47b991e5a3 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, common.Address{}, &toaddr, nil, params.Rules{}, params.CostPerStateByte) + cost, _ := IntrinsicGas(data, nil, nil, common.Address{}, &toaddr, nil, params.Rules{}) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { @@ -99,7 +99,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { Nonce: gen.TxNonce(benchRootAddr), To: &toaddr, Value: big.NewInt(1), - Gas: cost.RegularGas, + Gas: cost, Data: data, GasPrice: gasPrice, }) diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 9cbb489c4e..e275c07f3e 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -65,12 +65,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain @@ -102,11 +102,11 @@ func TestProcessUBT(t *testing.T) { txCost1 := params.TxGas txCost2 := params.TxGas - contractCreationCost := intrinsicContractCreationGas.RegularGas + + contractCreationCost := intrinsicContractCreationGas + params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* creation with value */ 739 /* execution costs */ - codeWithExtCodeCopyGas := intrinsicCodeWithExtCodeCopyGas.RegularGas + + codeWithExtCodeCopyGas := intrinsicCodeWithExtCodeCopyGas + params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (tx) */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (CREATE at pc=0x20) */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* write code hash */ diff --git a/core/eip2780_test.go b/core/eip2780_test.go index d9545923ff..cee5b3f685 100644 --- a/core/eip2780_test.go +++ b/core/eip2780_test.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -37,59 +38,67 @@ func TestEIP2780Intrinsic(t *testing.T) { name string to *common.Address value *uint256.Int - want vm.GasCosts + auths []types.SetCodeAuthorization + want uint64 }{ { name: "self-transfer", to: &from, value: uint256.NewInt(1), - want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + want: params.TxBaseCost2780, // 12,000 }, { name: "self-transfer/zero-value", to: &from, value: uint256.NewInt(0), - want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + want: params.TxBaseCost2780, // 12,000 }, { name: "zero-value call", to: &to, value: uint256.NewInt(0), - // TxBaseCost + ColdAccountAccess = 15,000 - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780}, + // TxBaseCost + ColdAccountAccess = 15,000; the recipient touch is + // charged at the cold rate unconditionally at the intrinsic phase. + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam, }, { name: "value transfer to existing EOA", to: &to, value: uint256.NewInt(1), // TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000 - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + - params.TxValueCost2780 + params.TransferLogCost2780}, + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + + params.TxValueCost2780 + params.TransferLogCost2780, }, { name: "contract creation, value = 0", to: nil, value: uint256.NewInt(0), - // TxBaseCost + CreateAccess = 23,000 regular, plus one account creation in state. - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + // TxBaseCost + CreateAccess = 23,000 regular. The new-account state + // charge depends on whether the deployment target exists and is + // charged at runtime, not intrinsically. + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam, }, { name: "contract creation, value > 0", to: nil, value: uint256.NewInt(1), - // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular, plus account creation. - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular. + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780, + }, + { + name: "value transfer with authorizations", + to: &to, + value: uint256.NewInt(1), + auths: make([]types.SetCodeAuthorization, 3), + // Each authorization adds the state-independent per-auth base + // (cold authority access included). + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + + params.TxValueCost2780 + params.TransferLogCost2780 + 3*params.RegularPerAuthBaseCost, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got, err := IntrinsicGas(nil, nil, nil, from, tc.to, tc.value, rules8037, params.CostPerStateByte) + got, err := IntrinsicGas(nil, nil, tc.auths, from, tc.to, tc.value, rules8037) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -105,7 +114,7 @@ func TestEIP2780Intrinsic(t *testing.T) { // (intrinsic + top-level + execution) recorded in the block gas pool. func TestEIP2780Gas(t *testing.T) { const ( - cold = params.ColdAccountAccess2780 + cold = params.ColdAccountAccessAmsterdam base = params.TxBaseCost2780 valueCst = params.TxValueCost2780 + params.TransferLogCost2780 ) @@ -154,9 +163,9 @@ func TestEIP2780Gas(t *testing.T) { // case 8: ETH transfer creating a new account. {"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState}, // case 9: contract-creation transaction, value = 0. - {"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccess2780, newAccountState}, + {"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccessAmsterdam, newAccountState}, // case 10: contract-creation transaction, value > 0. - {"create/value", valueCreateTx(1), base + params.CreateAccess2780 + params.TransferLogCost2780, newAccountState}, + {"create/value", valueCreateTx(1), base + params.CreateAccessAmsterdam + params.TransferLogCost2780, newAccountState}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -177,16 +186,191 @@ func TestEIP2780Gas(t *testing.T) { } } -// TestEIP2780NewAccountFunded verifies that a value transfer creating a new -// account both materializes and funds the recipient. -func TestEIP2780NewAccountFunded(t *testing.T) { - fresh := common.HexToAddress("0xbeef000000000000000000000000000000000002") - sdb := mkState(senderAlloc(nil)) - if _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 300_000, nil)); err != nil { +// callTxAL builds a signed dynamic-fee call carrying an access list. +func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types.AccessList) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value), + Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al, + }) +} + +// accessListEntryCost is the total intrinsic cost of one address-only access +// list entry: the EIP-8038 per-address charge plus the EIP-7981 data charge. +const accessListEntryCost = params.TxAccessListAddressGasAmsterdam + + common.AddressLength*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte + +// TestEIP2780WarmRecipientStillChargedCold verifies that a recipient warmed by +// the transaction's access list is still charged the recipient at the cold rate. +func TestEIP2780WarmRecipientStillChargedCold(t *testing.T) { + to := common.HexToAddress("0xe0a0000000000000000000000000000000000009") + sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) + al := types.AccessList{{Address: to}} + res, gp, err := applyMsg(t, sdb, callTxAL(0, to, 0, 100_000, al)) + if err != nil { t.Fatal(err) } - if !sdb.Exist(fresh) || sdb.GetBalance(fresh).Cmp(uint256.NewInt(1)) != 0 { - t.Fatalf("recipient not funded: exist=%v balance=%v", sdb.Exist(fresh), sdb.GetBalance(fresh)) + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (cold recipient, no access-list discount)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780DelegatedWarmTarget verifies that resolving the recipient's +// delegation is charged at the warm rate when the target was warmed by the +// access list, rather than the flat cold rate. +func TestEIP2780DelegatedWarmTarget(t *testing.T) { + var ( + target = common.HexToAddress("0x7a76000000000000000000000000000000000002") // codeless + delegated = common.HexToAddress("0xde1e000000000000000000000000000000000002") + ) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegated: {Code: types.AddressToDelegation(target)}, + })) + al := types.AccessList{{Address: target}} + res, gp, err := applyMsg(t, sdb, callTxAL(0, delegated, 0, 100_000, al)) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost + // recipient cold access (intrinsic) + params.WarmAccountAccessAmsterdam // warm delegation-target access (runtime) + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780RuntimeOOGRevertsDelegations verifies that running out of gas on +// a runtime authorization charge halts the transaction and reverts all state +// changes, including the already applied EIP-7702 delegations — while the +// sender's nonce increment persists. +// +// The halt burns the regular dimension in full; the state dimension is +// refilled by the revert and the reservoir — if any — is preserved and +// returned to the sender rather than burnt. +func TestEIP2780RuntimeOOGRevertsDelegations(t *testing.T) { + cases := []struct { + name string + gas uint64 + numAuths int + wantUsed uint64 // = gas − reservoir: all regular burnt, reservoir returned + }{ + // No state reservoir (gas below MaxTxGas). Gas covers the intrinsic + // cost (TX_BASE_COST + the cold-inclusive per-authorization base for + // a self-call) but not the runtime authorization charges + // (ACCOUNT_WRITE + account + indicator bytes): everything is burnt. + {"no-reservoir", 30_000, 1, 30_000}, + + // A 100,000 state reservoir (gas above MaxTxGas). The 100 + // authorizations' state charges (~21.9M) overwhelm the reservoir and + // the regular budget they spill into. The reservoir is made whole by + // the halt-refill and returned to the sender. + {"with-reservoir", params.MaxTxGas + 100_000, 100, params.MaxTxGas}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + auths = make([]types.SetCodeAuthorization, tc.numAuths) + authorities = make([]common.Address, tc.numAuths) + ) + for i := range auths { + key, _ := crypto.GenerateKey() + auth, err := types.SignSetCode(key, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + auths[i], authorities[i] = auth, crypto.PubkeyToAddress(key.PublicKey) + } + sdb := mkState(senderAlloc(nil)) + tx := types.MustSignNewTx(senderKey, signer8037, + &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), + Nonce: 0, + To: senderAddr, + Value: new(uint256.Int), + Gas: tc.gas, + GasFeeCap: new(uint256.Int), + GasTipCap: new(uint256.Int), + AuthList: auths, + }) + res, gp, err := applyMsg(t, sdb, tx) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err != vm.ErrOutOfGas { + t.Fatalf("expected out of gas, got %v", res.Err) + } + if res.UsedGas != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + // The charged state gas was refilled on the halt: the receipt is + // all regular, burnt in full, and only the reservoir survives. + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (refilled on halt)", gp.cumulativeState) + } + if gp.cumulativeRegular != tc.wantUsed { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed) + } + for i, authority := range authorities { + if code := sdb.GetCode(authority); len(code) != 0 { + t.Fatalf("delegation %d persisted despite runtime OOG: %x", i, code) + } + if sdb.GetNonce(authority) != 0 { + t.Fatalf("authority %d nonce persisted despite runtime OOG", i) + } + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } + }) + } +} + +// TestEIP2780SelfTransferDelegated verifies that a self-transfer incurs no +// recipient touch or value charges, while resolving the sender's own +// delegation is still paid for. +func TestEIP2780SelfTransferDelegated(t *testing.T) { + target := common.HexToAddress("0x7a76000000000000000000000000000000000003") // codeless + sdb := mkState(types.GenesisAlloc{ + senderAddr: {Balance: big.NewInt(1e18), Code: types.AddressToDelegation(target)}, + }) + res, gp, err := applyMsg(t, sdb, callTx(0, senderAddr, 1, 100_000, nil)) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam // base + cold delegation target + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (base + delegation resolution)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780CreateInsufficientStateGas verifies that a contract-creation +// transaction funded for its intrinsic gas but not the runtime new-account +// state charge is included, halts out of gas and consumes the nonce. +func TestEIP2780CreateInsufficientStateGas(t *testing.T) { + sdb := mkState(senderAlloc(nil)) + intrinsic := params.TxBaseCost2780 + params.CreateAccessAmsterdam // 23,000 + res, _, err := applyMsg(t, sdb, createTx(0, intrinsic, nil)) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err != vm.ErrOutOfGas { + t.Fatalf("expected out of gas, got %v", res.Err) + } + if res.UsedGas != intrinsic { + t.Fatalf("used gas = %d, want %d", res.UsedGas, intrinsic) + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") } } @@ -211,4 +395,405 @@ func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { if sdb.Exist(fresh) { t.Fatal("recipient should not be created when the call charge cannot be paid") } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } +} + +// TestEIP2780FirstFrameHaltPreservesPreExecution verifies the gas and state +// semantics when the top-most frame — message call or creation — halts +// exceptionally after the pre-execution phase completed: +// +// - state changes applied before the frame was entered persist together +// with their state-gas charge (the EIP-7702 delegations of a call tx); +// - state gas pre-charged for the frame itself is refilled when the halt +// voids it (the account-creation charge of a creation tx); +// - after the refill the regular dimension is burnt in full, while any +// remaining state reservoir is preserved and returned to the sender. +func TestEIP2780FirstFrameHaltPreservesPreExecution(t *testing.T) { + halting := common.HexToAddress("0xbad0000000000000000000000000000000000002") + cases := []struct { + name string + create bool + gas uint64 + wantUsed uint64 // = gas − preserved reservoir + wantRegular uint64 + wantState uint64 + }{ + // Message call carrying one authorization: the delegation and its + // state charge (account + indicator) survive the halt. + // + // Without a reservoir the charge spills from regular gas and everything is + // burnt; + // + // With a reservoir, the reservoir remainder is preserved. + {"call/no-reservoir", false, 1_000_000, 1_000_000, 1_000_000 - authWorstState, authWorstState}, + {"call/with-reservoir", false, params.MaxTxGas + 300_000, params.MaxTxGas + authWorstState, params.MaxTxGas, authWorstState}, + + // Creation whose init code halts: no durable account is created, so + // the pre-charged account creation is refilled and no state gas + // remains. + // + // Without a reservoir the refill repays spilled regular gas, which the + // halt then burns along with the rest; + // + // With a reservoir, the refill makes the reservoir whole again and it + // is preserved. + {"create/no-reservoir", true, 1_000_000, 1_000_000, 1_000_000, 0}, + {"create/with-reservoir", true, params.MaxTxGas + 100_000, params.MaxTxGas, params.MaxTxGas, 0}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + sdb := mkState(senderAlloc(types.GenesisAlloc{ + halting: {Code: []byte{0xfe}}, // INVALID + })) + var ( + tx *types.Transaction + authority common.Address + ) + if tc.create { + tx = createTx(0, tc.gas, []byte{0xfe}) // init code: INVALID + } else { + var auth types.SetCodeAuthorization + auth, authority = signAuth(t, authKeyA, delegate8037, 0) + tx = types.MustSignNewTx(senderKey, signer8037, + &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), + Nonce: 0, + To: halting, + Value: new(uint256.Int), + Gas: tc.gas, + GasFeeCap: new(uint256.Int), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + } + res, gp, err := applyMsg(t, sdb, tx) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err == nil { + t.Fatal("expected the frame to halt") + } + if res.UsedGas != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + if gp.cumulativeRegular != tc.wantRegular { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantRegular) + } + if gp.cumulativeState != tc.wantState { + t.Fatalf("state gas = %d, want %d", gp.cumulativeState, tc.wantState) + } + if tc.create { + // The halted creation is fully reverted: no durable account. + derived := crypto.CreateAddress(senderAddr, 0) + if code := sdb.GetCode(derived); len(code) != 0 { + t.Fatalf("created code persisted despite halt: %x", code) + } + if sdb.GetNonce(derived) != 0 { + t.Fatal("created account nonce persisted despite halt") + } + } else { + // The delegation applied before the frame was entered persists. + if code := sdb.GetCode(authority); len(code) == 0 { + t.Fatal("delegation should persist through an in-frame halt") + } + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } + }) + } +} + +// TestEIP2780CreatePreExecutionOOGPreservesReservoir verifies that when a +// creation transaction cannot afford the pre-execution account-creation state +// charge (before the init-code frame is entered), the transaction halts with +// all regular gas burnt while the state reservoir — never touched, since the +// charge is atomic and was not applied — is preserved and returned to the +// sender. +func TestEIP2780CreatePreExecutionOOGPreservesReservoir(t *testing.T) { + // Regular gas left for the pre-execution charge; together with the + // reservoir it must not cover the account-creation cost. + const ( + regularLeft = 100_000 + reservoir = 50_000 + ) + // Plain creation intrinsic: TX_BASE_COST + CREATE_ACCESS. + plainIntrinsic, err := IntrinsicGas(nil, nil, nil, senderAddr, nil, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) + } + // For the reservoir case the gas limit must exceed MaxTxGas, which leaves + // a huge regular budget by default. A big access list drives the intrinsic + // cost close to MaxTxGas, shrinking the regular budget back down to + // roughly regularLeft. Storage keys work because their intrinsic charge + // exceeds their EIP-7623/7976 floor contribution. + al := types.AccessList{{Address: common.HexToAddress("0xa1")}} + baseIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) + } + perKey := params.TxAccessListStorageKeyGasAmsterdam + uint64(common.HashLength)*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte + + // Fill the transaction with accessList, drain the gas and make it + // insufficient for account-creation cost. + al[0].StorageKeys = make([]common.Hash, (params.MaxTxGas-regularLeft-baseIntrinsic)/perKey) + alIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) + } + if left := params.MaxTxGas - alIntrinsic; left+reservoir >= newAccountState { + t.Fatalf("setup: regular %d + reservoir %d must not cover the creation charge %d", left, reservoir, newAccountState) + } + alCreateTx := types.MustSignNewTx(senderKey, signer8037, + &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, + Nonce: 0, + To: nil, + Value: big.NewInt(0), + Gas: params.MaxTxGas + reservoir, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + AccessList: al, + }) + + cases := []struct { + name string + tx *types.Transaction + wantUsed uint64 // = gas − preserved reservoir + }{ + // Gas below MaxTxGas: no reservoir, the whole limit is burnt. + {"no-reservoir", createTx(0, plainIntrinsic+regularLeft, nil), plainIntrinsic + regularLeft}, + + // Gas above MaxTxGas: the reservoir survives the halt untouched and + // is returned to the sender. + {"with-reservoir", alCreateTx, params.MaxTxGas}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + sdb := mkState(senderAlloc(nil)) + res, gp, err := applyMsg(t, sdb, tc.tx) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err != vm.ErrOutOfGas { + t.Fatalf("expected out of gas, got %v", res.Err) + } + if res.UsedGas != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + if gp.cumulativeRegular != tc.wantUsed { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed) + } + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState) + } + if derived := crypto.CreateAddress(senderAddr, 0); sdb.Exist(derived) { + t.Fatal("target account should not be created when the charge cannot be paid") + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } + }) + } +} + +// TestEIP2780AuthorityAccountWrite pins the first-write ACCOUNT_WRITE rule for +// authorities: the surcharge applies to the first paid write to the account +// within the transaction, regardless of whether the account exists, and is +// skipped when the write is already paid for: by TX_BASE_COST for the sender, +// by TX_VALUE_COST for the recipient of a value-bearing transaction, or by a +// preceding valid authorization. +func TestEIP2780AuthorityAccountWrite(t *testing.T) { + const ( + base = params.TxBaseCost2780 + cold = params.ColdAccountAccessAmsterdam + aw = params.AccountWriteAmsterdam + perAuth = params.RegularPerAuthBaseCost + valueCst = params.TxValueCost2780 + params.TransferLogCost2780 + ) + existingEOA := common.HexToAddress("0xe0a0000000000000000000000000000000000002") + + auth0, authority := signAuth(t, authKeyA, delegate8037, 0) + auth1, _ := signAuth(t, authKeyA, delegate8037, 1) + authBadNonce, _ := signAuth(t, authKeyA, delegate8037, 5) + + // Self-sponsored authorization: the sender's nonce is bumped before the + // authorization list is processed, hence nonce 1. + senderAuth, err := types.SignSetCode(senderKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 1, + }) + if err != nil { + t.Fatal(err) + } + // tx builds a SetCode transaction with an explicit value. + tx := func(to common.Address, value uint64, auths ...types.SetCodeAuthorization) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: 0, To: to, + Value: uint256.NewInt(value), Gas: 1_000_000, + GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths, + }) + } + fundedAuthority := types.GenesisAlloc{authority: {Balance: big.NewInt(1)}} + + cases := []struct { + name string + alloc types.GenesisAlloc + tx *types.Transaction + wantRegular, wantState uint64 + }{ + { + // Materializing a fresh authority pays the first-write surcharge + // alongside the new-account state gas and the indicator bytes. + name: "fresh authority", + tx: tx(existingEOA, 0, auth0), + wantRegular: base + cold + perAuth + aw, + wantState: authWorstState, + }, + { + // An existing authority still pays the surcharge: the nonce and + // indicator stores are the first write to the account within the + // transaction. + name: "existing authority", + alloc: fundedAuthority, + tx: tx(existingEOA, 0, auth0), + wantRegular: base + cold + perAuth + aw, + wantState: authBaseState, + }, + { + // Self-sponsored: the sender's account write is prepaid by + // TX_BASE_COST, no surcharge. + name: "authority is sender", + tx: tx(existingEOA, 0, senderAuth), + wantRegular: base + cold + perAuth, + wantState: authBaseState, + }, + { + // authority == tx.to with zero value: no TX_VALUE_COST was paid, + // so the authorization write is the first paid write and the + // surcharge applies. The recipient becomes delegated, adding a + // cold delegation-target access at runtime. + name: "authority is recipient, zero value", + alloc: fundedAuthority, + tx: tx(authority, 0, auth0), + wantRegular: base + cold + perAuth + aw + cold, + wantState: authBaseState, + }, + { + // authority == tx.to with value: TX_VALUE_COST prepaid the + // recipient write, so no surcharge is due. + name: "authority is recipient, value", + alloc: fundedAuthority, + tx: tx(authority, 1, auth0), + wantRegular: base + cold + valueCst + perAuth + cold, + wantState: authBaseState, + }, + { + // Fresh authority == tx.to with value: the authorization pays the + // new-account state gas, and the recipient charge then sees an + // existing account, so the leaf is not paid for twice. + name: "authority is fresh recipient, value", + tx: tx(authority, 1, auth0), + wantRegular: base + cold + valueCst + perAuth + cold, + wantState: authWorstState, + }, + { + // The same authority twice: only the first valid authorization + // carries the surcharge, the account creation and the indicator. + name: "same authority twice", + tx: tx(existingEOA, 0, auth0, auth1), + wantRegular: base + cold + 2*perAuth + aw, + wantState: authWorstState, + }, + { + // An invalid authorization performs no write and does not count + // as the first write; the following valid one pays in full. The + // per-auth intrinsic base is still paid for the invalid tuple. + name: "invalid then valid", + tx: tx(existingEOA, 0, authBadNonce, auth0), + wantRegular: base + cold + 2*perAuth + aw, + wantState: authWorstState, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + alloc := types.GenesisAlloc{existingEOA: {Balance: big.NewInt(1)}} + for addr, acc := range tc.alloc { + alloc[addr] = acc + } + res, gp, err := applyMsg(t, mkState(senderAlloc(alloc)), tc.tx) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if gp.cumulativeRegular != tc.wantRegular { + t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular) + } + if gp.cumulativeState != tc.wantState { + t.Errorf("state gas = %d, want %d", gp.cumulativeState, tc.wantState) + } + }) + } +} + +// TestEIP2780DelegationTargetPrewarmed pins the warm rate for delegation +// targets that are already in accessed_addresses when the recipient is +// loaded. +func TestEIP2780DelegationTargetPrewarmed(t *testing.T) { + const ( + base = params.TxBaseCost2780 + cold = params.ColdAccountAccessAmsterdam + warm = params.WarmAccountAccessAmsterdam + aw = params.AccountWriteAmsterdam + perAuth = params.RegularPerAuthBaseCost + ) + delegatedAcct := common.HexToAddress("0xde1e000000000000000000000000000000000002") + + t.Run("target is sender", func(t *testing.T) { + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegatedAcct: {Code: types.AddressToDelegation(senderAddr)}, + })) + res, gp, err := applyMsg(t, sdb, callTx(0, delegatedAcct, 0, 100_000, nil)) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if want := base + cold + warm; gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want) + } + if gp.cumulativeState != 0 { + t.Errorf("state gas = %d, want 0", gp.cumulativeState) + } + }) + + t.Run("target warmed by authorization", func(t *testing.T) { + // A clearing authorization from a fresh authority: it creates the + // authority account (nonce bump) and warms it, without installing an + // indicator. + // + // The recipient's pre-existing delegation then resolves to + // the freshly warmed, codeless authority at the warm rate. + authClear, authority := signAuth(t, authKeyA, common.Address{}, 0) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegatedAcct: {Code: types.AddressToDelegation(authority)}, + })) + res, gp, err := applyMsg(t, sdb, setCodeTx(0, delegatedAcct, []types.SetCodeAuthorization{authClear})) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if want := base + cold + perAuth + aw + warm; gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (auth-warmed delegation target)", gp.cumulativeRegular, want) + } + if gp.cumulativeState != newAccountState { + t.Errorf("state gas = %d, want %d (authority account created)", gp.cumulativeState, newAccountState) + } + }) } diff --git a/core/eip8037_test.go b/core/eip8037_test.go index 6d506cba96..2b56749f0b 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -21,6 +21,7 @@ package core import ( + "errors" "math/big" "testing" @@ -72,6 +73,40 @@ func mkState(alloc types.GenesisAlloc) *state.StateDB { return sdb } +// mkCommittedState is mkState with the allocation committed to disk and +// reloaded. EIP-161-empty accounts carrying only storage do not survive an +// in-memory Finalise; committing without empty-account deletion reproduces +// the synthesized prestate an EIP-7610 fixture would load from disk. +func mkCommittedState(t *testing.T, alloc types.GenesisAlloc) *state.StateDB { + t.Helper() + db := state.NewDatabaseForTesting() + sdb, _ := state.New(types.EmptyRootHash, db) + for addr, acc := range alloc { + sdb.CreateAccount(addr) + if acc.Balance != nil { + sdb.AddBalance(addr, uint256.MustFromBig(acc.Balance), tracing.BalanceChangeUnspecified) + } + if acc.Nonce != 0 { + sdb.SetNonce(addr, acc.Nonce, tracing.NonceChangeGenesis) + } + if len(acc.Code) != 0 { + sdb.SetCode(addr, acc.Code, tracing.CodeChangeUnspecified) + } + for k, v := range acc.Storage { + sdb.SetState(addr, k, v) + } + } + root, err := sdb.Commit(0, false, false) + if err != nil { + t.Fatalf("commit prestate: %v", err) + } + sdb, err = state.New(root, db) + if err != nil { + t.Fatalf("reopen prestate: %v", err) + } + return sdb +} + // amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled. func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM { ctx := vm.BlockContext{ @@ -99,15 +134,24 @@ func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*Executi t.Fatalf("to message: %v", err) } gp := NewGasPool(evm.Context.GasLimit) - // Drive the stateTransition directly (as ApplyMessage does) so the test can - // inspect the final tx-level GasBudget vector via st.gasRemaining. + evm.SetTxContext(NewEVMTxContext(msg)) st := newStateTransition(evm, msg, gp) res, err := st.execute() if err == nil && res != nil { - assertPoolSane(t, res, gp) - limit := min(msg.GasLimit, params.MaxTxGas) - assertBudgetSane(t, vm.NewGasBudget(limit, msg.GasLimit-limit), st.gasRemaining) + floor, ferr := FloorDataGas(rules8037, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList) + if ferr != nil { + t.Fatalf("floor data gas: %v", ferr) + } + assertPoolSane(t, res, gp, floor) + + intrinsic, ierr := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037) + if ierr != nil { + t.Fatalf("intrinsic gas: %v", ierr) + } + executionGas := msg.GasLimit - intrinsic + gasLeft := min(params.MaxTxGas-intrinsic, executionGas) + assertBudgetSane(t, vm.NewGasBudget(gasLeft, executionGas-gasLeft), st.gasRemaining) } return res, gp, err } @@ -136,9 +180,11 @@ func assertBudgetSane(t *testing.T, initial, got vm.GasBudget) { // assertPoolSane validates the whole 2D block-gas-pool vector after a single tx. // // receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas -// pre-refund: cumulativeRegular + cumulativeState <= res.MaxUsedGas (peak) +// regular: cumulativeRegular <= max(res.MaxUsedGas - cumulativeState, floor) +// (the calldata floor pads the regular dimension alone, so the +// dimension sum may exceed the pre-refund peak when it binds) // bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial -func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) { +func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool, floor uint64) { t.Helper() if gp.cumulativeUsed != res.UsedGas { t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas) @@ -146,8 +192,12 @@ func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) { if res.UsedGas > res.MaxUsedGas { t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas) } - if sum := gp.cumulativeRegular + gp.cumulativeState; sum > res.MaxUsedGas { - t.Fatalf("regular+state %d exceeds peak %d", sum, res.MaxUsedGas) + if gp.cumulativeState > res.MaxUsedGas { + t.Fatalf("state %d exceeds peak %d", gp.cumulativeState, res.MaxUsedGas) + } + if cap := max(res.MaxUsedGas-gp.cumulativeState, floor); gp.cumulativeRegular > cap { + t.Fatalf("regular %d exceeds pre-refund cap %d (peak %d, state %d, floor %d)", + gp.cumulativeRegular, cap, res.MaxUsedGas, gp.cumulativeState, floor) } if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) { t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState) @@ -185,23 +235,26 @@ func createTx(nonce, gas uint64, initCode []byte) *types.Transaction { var ( deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT + haltI = []byte{0xfe, 0x00, 0x00, 0x00, 0x00} // init: INVALID, exceptional halt ) // ===================== Top-level create transaction ====================== -// A creation tx's intrinsic gas pre-charges one account creation as state gas. -func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte) +// A creation tx's intrinsic gas is state-independent: the new-account state +// charge depends on whether the deployment target exists and is charged at +// runtime (EIP-2780), not intrinsically. +func TestCreateTxIntrinsicNoStateGas(t *testing.T) { + cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037) if err != nil { t.Fatal(err) } - if cost.StateGas != newAccountState { - t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState) + if want := params.TxBaseCost2780 + params.CreateAccessAmsterdam; cost != want { + t.Fatalf("intrinsic gas = %d, want %d", cost, want) } } -// Creating onto a pre-existing (balance-only) address refills the account -// portion; only the code deposit is charged as state gas. +// Creating onto a pre-existing (balance-only) address incurs no new-account +// runtime charge; only the code deposit is charged as state gas. func TestCreateTxPreexistingDestRefill(t *testing.T) { derived := crypto.CreateAddress(senderAddr, 0) sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}})) @@ -214,7 +267,8 @@ func TestCreateTxPreexistingDestRefill(t *testing.T) { } } -// A creation tx that reverts refills the account-creation charge. +// A creation tx that reverts refills the account-creation charge applied at +// runtime. func TestCreateTxRevertRefill(t *testing.T) { sdb := mkState(senderAlloc(nil)) res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI)) @@ -229,7 +283,8 @@ func TestCreateTxRevertRefill(t *testing.T) { } } -// An address collision burns gas_left while refilling the account charge. +// An address collision burns gas_left. The colliding target exists, so no +// new-account state gas is charged at runtime in the first place. func TestCreateTxCollisionConsumesGasLeft(t *testing.T) { const gas = 1_000_000 derived := crypto.CreateAddress(senderAddr, 0) @@ -241,14 +296,185 @@ func TestCreateTxCollisionConsumesGasLeft(t *testing.T) { if !res.Failed() { t.Fatal("expected collision failure") } + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (never charged)", gp.cumulativeState) + } + // All forwarded gas_left is burned: the whole gas limit is consumed as + // regular gas. + if want := uint64(gas); gp.cumulativeRegular != want { + t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want) + } +} + +// An account can exist yet be EIP-161-empty in the middle of a transaction, +// e.g. after being touched as the zero-balance beneficiary of a SELFDESTRUCT. +// Deploying onto such an account should charge account-creation cost. +func TestCreate2TransientEmptyDestNoRefill(t *testing.T) { + var ( + orchestrator = common.HexToAddress("0xc0de000000000000000000000000000000000002") + destructor = common.HexToAddress("0xc0de000000000000000000000000000000000003") + target = crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(deploy3)) + ) + // destructor: SELFDESTRUCT with zero balance to the future CREATE2 target, + // leaving it existing but EIP-161-empty for the rest of the transaction. + destructorCode := append(append([]byte{0x73}, target.Bytes()...), 0xff) // PUSH20 target, SELFDESTRUCT + + // orchestrator: CALL destructor (persist the success flag in slot 0), + // then CREATE2 deploy3 with salt 0, targeting the touched address. + code := []byte{ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, // ret/arg sizes and offsets, value = 0 + 0x73, // PUSH20 destructor + } + code = append(code, destructor.Bytes()...) + code = append(code, + 0x62, 0x03, 0x0d, 0x40, // PUSH3 200,000 call gas + 0xf1, // CALL + 0x60, 0x00, 0x55, // SSTORE the call result at slot 0 + 0x64, 0x60, 0x03, 0x60, 0x00, 0xf3, // PUSH5 deploy3 init code + 0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27) + 0x60, 0x00, // salt = 0 + 0x60, 0x05, // size = 5 + 0x60, 0x1b, // offset = 27 + 0x60, 0x00, // endowment = 0 + 0xf5, 0x50, // CREATE2, POP + 0x00, // STOP + ) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + orchestrator: {Code: code}, + destructor: {Code: destructorCode}, + })) + res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 2_000_000, nil)) + if err != nil { + t.Fatal(err) + } + if res.Failed() { + t.Fatalf("execution failed: %v", res.Err) + } + // The inner call must have succeeded, so the target was touched into an + // existing-but-empty account before the CREATE2 executed. + if flag := sdb.GetState(orchestrator, common.Hash{}); flag != common.BigToHash(big.NewInt(1)) { + t.Fatalf("destructor call flag = %v, want 1", flag) + } + if code := sdb.GetCode(target); len(code) != 3 { + t.Fatalf("deployed code length = %d, want 3", len(code)) + } + // State gas: the orchestrator's flag slot, the created contract account + // (charged, not refilled) and the 3-byte code deposit. + want := newSlotState + newAccountState + uint64(3*params.CostPerStateByte) + if gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d (account creation must not be refilled)", gp.cumulativeState, want) + } +} + +// ========== Storage-only (EIP-7610-shaped) deployment destination =========== +// +// A destination carrying storage while having zero nonce, zero balance and +// empty code is EIP-161-empty, so the account-creation state gas is +// pre-charged in the parent frame. + +// create2Orchestrator returns runtime code that CREATE2-deploys the given +// 5-byte init code with salt 0 and stores the result address at slot 0. +func create2Orchestrator(initCode []byte) []byte { + code := append([]byte{0x64}, initCode...) // PUSH5 init code + return append(code, + 0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27) + 0x60, 0x00, // salt = 0 + 0x60, 0x05, // size = 5 + 0x60, 0x1b, // offset = 27 + 0x60, 0x00, // endowment = 0 + 0xf5, // CREATE2 + 0x60, 0x00, 0x55, // SSTORE the result address at slot 0 + 0x00, // STOP + ) +} + +// storageOnlyAlloc allocates the orchestrator and its CREATE2 target, the +// latter carrying a single storage slot while remaining EIP-161-empty. +func storageOnlyAlloc(orchestrator common.Address, initCode []byte) (types.GenesisAlloc, common.Address) { + target := crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(initCode)) + return types.GenesisAlloc{ + orchestrator: {Code: create2Orchestrator(initCode)}, + target: {Storage: map[common.Hash]common.Hash{{}: common.BigToHash(big.NewInt(1))}}, + }, target +} + +// Deploying onto a storage-only destination pre-charges the account creation. +// Under the registry-based EIP-7610 check the creation proceeds, so the +// charge is consumed like any other creation. +func TestCreate2StorageOnlyDestCharged(t *testing.T) { + orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000004") + alloc, target := storageOnlyAlloc(orchestrator, deploy3) + sdb := mkCommittedState(t, senderAlloc(alloc)) + res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 1_000_000, nil)) + if err != nil { + t.Fatal(err) + } + if res.Failed() { + t.Fatalf("execution failed: %v", res.Err) + } + if code := sdb.GetCode(target); len(code) != 3 { + t.Fatalf("deployed code length = %d, want 3", len(code)) + } + // The created account (charged, consumed), the orchestrator's result slot + // and the 3-byte code deposit. + want := newAccountState + newSlotState + uint64(3*params.CostPerStateByte) + if gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want) + } +} + +// If the pre-charge succeeds and the create frame then fails, only the create +// frame halts: the forwarded regular gas is burnt, the account-creation +// charge is refilled, and the parent frame continues. +func TestCreate2StorageOnlyDestRefillOnFrameHalt(t *testing.T) { + const gas = 1_000_000 + orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000005") + alloc, target := storageOnlyAlloc(orchestrator, haltI) + sdb := mkCommittedState(t, senderAlloc(alloc)) + res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil)) + if err != nil { + t.Fatal(err) + } + if res.Failed() { + t.Fatalf("parent frame must survive the create-frame halt: %v", res.Err) + } + // The CREATE2 pushed zero and nothing was deployed. + if flag := sdb.GetState(orchestrator, common.Hash{}); flag != (common.Hash{}) { + t.Fatalf("create result = %v, want 0", flag) + } + if code := sdb.GetCode(target); len(code) != 0 { + t.Fatalf("deployed code length = %d, want 0", len(code)) + } + // The account-creation charge was refilled in full. if gp.cumulativeState != 0 { t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState) } - // All forwarded gas_left is burned; only the refilled account charge (which - // had spilled into regular) returns to gas_left. So regular gas consumed is - // exactly tx.gas - newAccountState, with no other refund. - if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want { - t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want) + if res.UsedGas > gas-newAccountState { + t.Fatalf("used gas = %d, want at most %d (charge not refilled?)", res.UsedGas, gas-newAccountState) + } +} + +// If the remaining gas cannot cover the account-creation pre-charge, the +// parent frame itself halts with out-of-gas instead of the create frame. +func TestCreate2StorageOnlyDestPrechargeOOG(t *testing.T) { + // Enough for the CREATE2 constant cost, short of the 183,600 pre-charge. + const gas = 150_000 + orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000006") + alloc, _ := storageOnlyAlloc(orchestrator, deploy3) + sdb := mkCommittedState(t, senderAlloc(alloc)) + res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil)) + if err != nil { + t.Fatal(err) + } + if !res.Failed() || !errors.Is(res.Err, vm.ErrOutOfGas) { + t.Fatalf("err = %v, want out of gas in the parent frame", res.Err) + } + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState) + } + // The parent is the topmost frame, so its halt burns the whole gas limit. + if gp.cumulativeRegular != gas { + t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, gas) } } @@ -300,15 +526,50 @@ func TestValidationIntrinsicRegularCap(t *testing.T) { for i := range al { al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1))) } - tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ - ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0), - Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al, - }) + tx := types.MustSignNewTx(senderKey, signer8037, + &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, + Nonce: 0, + To: &senderAddr, + Value: big.NewInt(0), + Gas: 25_000_000, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + AccessList: al, + }) if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil { t.Fatal("expected rejection for intrinsic regular over MaxTxGas") } } +// The EIP-7623/7976 calldata floor is capped by MaxTxGas even when the gas +// limit covers it: a transaction whose floor cost exceeds the cap is rejected +// regardless of its (much smaller) intrinsic gas. +func TestValidationFloorCostCap(t *testing.T) { + // All-zero calldata: the floor charges 64/byte while the intrinsic + // charges only 4/byte, so the floor crosses the cap long before the + // intrinsic does. + data := make([]byte, 300_000) // floor ~19.2M > 16.77M cap, intrinsic ~1.2M + floor, err := FloorDataGas(rules8037, senderAddr, &senderAddr, new(uint256.Int), data, nil) + if err != nil { + t.Fatal(err) + } + intrinsic, err := IntrinsicGas(data, nil, nil, senderAddr, &senderAddr, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) + } + if floor <= params.MaxTxGas || intrinsic > params.MaxTxGas { + t.Fatalf("setup: floor %d must exceed cap %d while intrinsic %d stays below", + floor, params.MaxTxGas, intrinsic) + } + // The gas limit covers the floor, so the rejection can only come from + // the MaxTxGas cap on the floor cost. + tx := callTx(0, senderAddr, 0, floor+1_000_000, data) + if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); !errors.Is(err, ErrFloorDataGas) { + t.Fatalf("expected ErrFloorDataGas, got %v", err) + } +} + // ========================= Refund and gas used =========================== // clearSlots deploys a contract that zeroes slots 1..n, each preset to 1. @@ -472,18 +733,23 @@ const authKeyA = "02020202020202020202020202020202020202020202020202020020202020 var delegate8037 = common.HexToAddress("0xde1e8a7e") -// Intrinsic gas pre-charges the worst-case (account + indicator) per auth. -func TestAuthIntrinsicWorstCase(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte) +// Intrinsic gas charges only the state-independent per-authorization base; +// the state-dependent charges are applied at runtime (EIP-2780). +func TestAuthIntrinsicBaseOnly(t *testing.T) { + cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037) if err != nil { t.Fatal(err) } - if cost.StateGas != authWorstState { - t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState) + // The recipient touch and the per-authorization authority access (priced + // into RegularPerAuthBaseCost) are both charged at the cold rate + // unconditionally at the intrinsic phase (EIP-2780). + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + params.RegularPerAuthBaseCost + if cost != want { + t.Fatalf("intrinsic gas = %d, want %d", cost, want) } } -// An invalid authorization refills its entire intrinsic state-gas charge. +// An invalid authorization incurs no runtime state-gas charge. func TestAuthInvalidRefillFull(t *testing.T) { k, _ := crypto.HexToECDSA(authKeyA) bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ @@ -499,7 +765,8 @@ func TestAuthInvalidRefillFull(t *testing.T) { } } -// A pre-existing authority refills the account portion (indicator stands). +// A pre-existing authority is not charged for an account leaf; only the +// net-new indicator bytes are charged at runtime. func TestAuthAccountExistsRefill(t *testing.T) { auth, authority := signAuth(t, authKeyA, delegate8037, 0) sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})) @@ -508,12 +775,12 @@ func TestAuthAccountExistsRefill(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != authBaseState { - t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState) + t.Fatalf("state gas = %d, want %d (indicator only)", gp.cumulativeState, authBaseState) } } -// Setting a delegation on an already-delegated authority refills the indicator -// portion (and the account portion, since the authority already exists). +// Setting a delegation on an already-delegated authority writes no net-new +// bytes (and no account leaf, since the authority exists): no state charge. func TestAuthSetOnDelegatedRefillBase(t *testing.T) { auth, authority := signAuth(t, authKeyA, delegate8037, 0) pre := types.AddressToDelegation(common.HexToAddress("0xabcd")) @@ -523,11 +790,12 @@ func TestAuthSetOnDelegatedRefillBase(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != 0 { - t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState) + t.Fatalf("state gas = %d, want 0 (nothing net-new)", gp.cumulativeState) } } -// A net-new delegation on a fresh authority keeps the full worst-case charge. +// A net-new delegation on a fresh authority is charged the account leaf plus +// the indicator bytes at runtime. func TestAuthSetNetNewNoRefill(t *testing.T) { auth, _ := signAuth(t, authKeyA, delegate8037, 0) sdb := mkState(senderAlloc(nil)) @@ -536,11 +804,12 @@ func TestAuthSetNetNewNoRefill(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != authWorstState { - t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState) + t.Fatalf("state gas = %d, want %d (leaf + indicator)", gp.cumulativeState, authWorstState) } } -// Clearing a delegation writes no indicator, so the indicator portion refills. +// Clearing a delegation writes no indicator, so only the (new) account leaf is +// charged at runtime. func TestAuthClearRefillBase(t *testing.T) { auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO) sdb := mkState(senderAlloc(nil)) @@ -549,13 +818,14 @@ func TestAuthClearRefillBase(t *testing.T) { t.Fatal(err) } if want := newAccountState; gp.cumulativeState != want { - t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want) + t.Fatalf("state gas = %d, want %d (account leaf only)", gp.cumulativeState, want) } } -// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a -// later one writes zero net bytes, so both indicator charges refill. -func TestAuthClearSameTxDoubleRefill(t *testing.T) { +// 0->a->0 in one tx: the indicator charge applies when the delegation is set +// and is never credited back when a later auth clears it in the same +// transaction. +func TestAuthClearSameTxNoRefill(t *testing.T) { set, authority := signAuth(t, authKeyA, delegate8037, 0) clr, _ := signAuth(t, authKeyA, common.Address{}, 1) sdb := mkState(senderAlloc(nil)) @@ -564,8 +834,28 @@ func TestAuthClearSameTxDoubleRefill(t *testing.T) { t.Fatal(err) } _ = authority - if want := newAccountState; gp.cumulativeState != want { - t.Fatalf("state gas = %d, want %d (net-zero delegation)", gp.cumulativeState, want) + if want := authWorstState; gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d (indicator charge kept on clear)", gp.cumulativeState, want) + } +} + +// 0->a->0->b in one tx: the indicator charge applies at most once per +// authority — re-installing a delegation after an intra-tx clear is free. +func TestAuthSetClearSetChargedOnce(t *testing.T) { + set, _ := signAuth(t, authKeyA, delegate8037, 0) + clr, _ := signAuth(t, authKeyA, common.Address{}, 1) + set2, authority := signAuth(t, authKeyA, common.HexToAddress("0xde1e8a7f"), 2) + sdb := mkState(senderAlloc(nil)) + _, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{set, clr, set2})) + if err != nil { + t.Fatal(err) + } + // The final delegation is installed and the indicator was paid exactly once. + if _, delegated := types.ParseDelegation(sdb.GetCode(authority)); !delegated { + t.Fatal("final delegation not installed") + } + if want := authWorstState; gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d (leaf + indicator exactly once)", gp.cumulativeState, want) } } diff --git a/core/eip8038_test.go b/core/eip8038_test.go index d8977c6dd2..ca42d1498b 100644 --- a/core/eip8038_test.go +++ b/core/eip8038_test.go @@ -14,14 +14,6 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// EIP-8038 authorization accounting tests. The per-authorization intrinsic gas -// pre-charges ACCOUNT_WRITE (regular) on top of REGULAR_PER_AUTH_BASE_COST. -// applyAuthorization refunds that ACCOUNT_WRITE to the refund counter in exactly -// the cases where no new account leaf is written: an invalid authorization, or -// an authority whose account already exists. These white-box tests invoke -// applyAuthorization directly and read the raw refund counter, so they observe -// the refund before the EIP-3529 cap is applied. - package core import ( @@ -37,74 +29,117 @@ import ( "github.com/holiman/uint256" ) -// newAuthTestTransition builds a minimal stateTransition with a state reservoir, -// suitable for calling applyAuthorization directly. +// newAuthTestTransition builds a minimal stateTransition with a runtime gas +// budget, suitable for calling applyAuthorization directly. func newAuthTestTransition(sdb *state.StateDB) *stateTransition { st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000)) - st.gasRemaining = vm.NewGasBudget(0, 1_000_000) // reservoir for state-gas refills + st.gasRemaining = vm.NewGasBudget(1_000_000, 1_000_000) return st } -// A net-new delegation on a fresh authority writes a new account leaf, so the -// intrinsic ACCOUNT_WRITE stands (no refund). -func TestAuthAccountWriteNetNewNoRefund(t *testing.T) { +// A net-new delegation on a fresh, cold authority is charged ACCOUNT_WRITE in +// regular gas (the authority's cold access is paid unconditionally at the +// intrinsic phase, not here), plus the account leaf and the indicator bytes in +// state gas. +func TestAuthRuntimeChargeNetNew(t *testing.T) { auth, _ := signAuth(t, authKeyA, delegate8037, 0) st := newAuthTestTransition(mkState(senderAlloc(nil))) - if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authTracking{}); err != nil { t.Fatal(err) } - if got := st.state.GetRefund(); got != 0 { - t.Fatalf("refund = %d, want 0 (net-new account write)", got) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) } } -// A pre-existing authority writes no new account leaf, so the intrinsic -// ACCOUNT_WRITE is refunded. -func TestAuthAccountWriteExistsRefund(t *testing.T) { +// A pre-existing authority writes no new account leaf, but its first write in +// the transaction still carries ACCOUNT_WRITE; the authority's cold access is +// paid at the intrinsic phase, so only the net-new indicator bytes are charged +// as state gas here. +func TestAuthRuntimeChargeExistingAccount(t *testing.T) { auth, authority := signAuth(t, authKeyA, delegate8037, 0) st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) - if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authTracking{}); err != nil { t.Fatal(err) } - if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { - t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) } } -// An invalid authorization is skipped without writing any account leaf, so its -// intrinsic ACCOUNT_WRITE is refunded. -func TestAuthAccountWriteInvalidRefund(t *testing.T) { +// No cold surcharge is ever charged at runtime — the authority access is priced +// at the intrinsic phase — so an authority already warmed by the access list or +// an earlier authorization pays only the first-write surcharge, as it would +// whether warm or cold. +func TestAuthRuntimeChargeWarmAuthority(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) + st.state.AddAddressToAccessList(authority) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authTracking{}); err != nil { + t.Fatal(err) + } + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d (warm authority)", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) + } +} + +// An invalid authorization is skipped without any runtime charge. +func TestAuthRuntimeInvalidNoCharge(t *testing.T) { k, _ := crypto.HexToECDSA(authKeyA) bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id }) st := newAuthTestTransition(mkState(senderAlloc(nil))) - if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil { + if err := st.applyAuthorization(rules8037, &bad, map[common.Address]*authTracking{}); err == nil { t.Fatal("expected invalid-authorization error") } - if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { - t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam) + if st.gasRemaining.UsedRegularGas != 0 || st.gasRemaining.UsedStateGas != 0 { + t.Fatalf("charged = <%d,%d>, want <0,0> (invalid authorization)", + st.gasRemaining.UsedRegularGas, st.gasRemaining.UsedStateGas) } } -// The same authority across two authorizations writes its account leaf only -// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the -// account as existing) is refunded. -func TestAuthAccountWriteDuplicateOnce(t *testing.T) { +// The same authority across two authorizations is charged once: the first auth +// warms the authority, materializes the account and installs the indicator, so +// the second incurs no further charge. +func TestAuthRuntimeDuplicateAuthorityOnce(t *testing.T) { a0, _ := signAuth(t, authKeyA, delegate8037, 0) a1, _ := signAuth(t, authKeyA, delegate8037, 1) st := newAuthTestTransition(mkState(senderAlloc(nil))) - delegates := map[common.Address]bool{} - if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil { + authorities := map[common.Address]*authTracking{} + if err := st.applyAuthorization(rules8037, &a0, authorities); err != nil { t.Fatal(err) } - if got := st.state.GetRefund(); got != 0 { - t.Fatalf("refund after first auth = %d, want 0", got) - } - if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil { + if err := st.applyAuthorization(rules8037, &a1, authorities); err != nil { t.Fatal(err) } - if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { - t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d (once)", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d (once)", st.gasRemaining.UsedStateGas, want) + } +} + +// A budget that cannot cover the runtime charge aborts authorization +// processing with ErrOutOfGasRuntime, without mutating the authority. +func TestAuthRuntimeOutOfGas(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + st.gasRemaining = vm.NewGasBudget(10_000, 0) // covers neither leaf nor indicator + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authTracking{}); err != ErrOutOfGasRuntime { + t.Fatalf("err = %v, want ErrOutOfGasRuntime", err) + } + if st.state.GetNonce(authority) != 0 || len(st.state.GetCode(authority)) != 0 { + t.Fatal("authority mutated despite out-of-gas runtime charge") } } diff --git a/core/error.go b/core/error.go index 26b007f9d9..446ae4757a 100644 --- a/core/error.go +++ b/core/error.go @@ -137,4 +137,9 @@ var ( ErrAuthorizationInvalidSignature = errors.New("EIP-7702 authorization has invalid signature") ErrAuthorizationDestinationHasCode = errors.New("EIP-7702 authorization destination is a contract") ErrAuthorizationNonceMismatch = errors.New("EIP-7702 authorization nonce does not match current account nonce") + + // ErrOutOfGasRuntime is returned when the transaction's gas budget cannot + // cover an EIP-2780 runtime charge. The transaction remains valid: the top + // frame halts out of gas and its state changes are reverted. + ErrOutOfGasRuntime = errors.New("out of gas covering EIP-2780 runtime charge") ) diff --git a/core/state_transition.go b/core/state_transition.go index 4967675164..48a1bcd842 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -68,33 +69,28 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, from common.Address, to *common.Address, value *uint256.Int, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, from common.Address, to *common.Address, value *uint256.Int, rules params.Rules) (uint64, error) { isContractCreation := to == nil // Set the starting gas for the raw transaction - var gas vm.GasCosts + var gas uint64 if rules.IsAmsterdam { - gas.RegularGas = intrinsicBaseGasEIP2780(from, to, value) - if isContractCreation { - // New-account creation is charged as state gas (EIP-8037). - gas.StateGas = params.AccountCreationSize * costPerStateByte - } + gas = intrinsicBaseGasEIP2780(from, to, value) } else if isContractCreation && rules.IsHomestead { - gas.RegularGas = params.TxGasContractCreation + gas = params.TxGasContractCreation } else { - gas.RegularGas = params.TxGas + gas = params.TxGas } // Add gas for authorizations if authList != nil { if rules.IsAmsterdam { - gas.RegularGas += uint64(len(authList)) * (params.AccountWriteAmsterdam + params.RegularPerAuthBaseCost) - gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte + gas += uint64(len(authList)) * params.RegularPerAuthBaseCost } else { - gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas + gas += uint64(len(authList)) * params.CallNewAccountGas } } - dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data + dataLen := uint64(len(data)) if dataLen > 0 { // Zero and non-zero bytes are priced differently z := uint64(bytes.Count(data, []byte{0})) @@ -105,24 +101,25 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set if rules.IsIstanbul { nonZeroGas = params.TxDataNonZeroGasEIP2028 } - if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz { - return vm.GasCosts{}, ErrGasUintOverflow + if (math.MaxUint64-gas)/nonZeroGas < nz { + return 0, ErrGasUintOverflow } - gas.RegularGas += nz * nonZeroGas + gas += nz * nonZeroGas - if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z { - return vm.GasCosts{}, ErrGasUintOverflow + if (math.MaxUint64-gas)/params.TxDataZeroGas < z { + return 0, ErrGasUintOverflow } - gas.RegularGas += z * params.TxDataZeroGas + gas += z * params.TxDataZeroGas if isContractCreation && rules.IsShanghai { lenWords := toWordSize(dataLen) - if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords { - return vm.GasCosts{}, ErrGasUintOverflow + if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { + return 0, ErrGasUintOverflow } - gas.RegularGas += lenWords * params.InitCodeWordGas + gas += lenWords * params.InitCodeWordGas } } + // Add the gas for accessList if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) @@ -134,14 +131,14 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set addressCost = params.TxAccessListAddressGasAmsterdam storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam } - if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { - return vm.GasCosts{}, ErrGasUintOverflow + if (math.MaxUint64-gas)/addressCost < addresses { + return 0, ErrGasUintOverflow } - gas.RegularGas += addresses * addressCost - if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { - return vm.GasCosts{}, ErrGasUintOverflow + gas += addresses * addressCost + if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + return 0, ErrGasUintOverflow } - gas.RegularGas += storageKeys * storageKeyCost + gas += storageKeys * storageKeyCost // EIP-7981: access list data is charged in addition to the base charge. if rules.IsAmsterdam { @@ -149,38 +146,41 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte ) - if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { - return vm.GasCosts{}, ErrGasUintOverflow + if (math.MaxUint64-gas)/addressCost < addresses { + return 0, ErrGasUintOverflow } - gas.RegularGas += addresses * addressCost - if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { - return vm.GasCosts{}, ErrGasUintOverflow + gas += addresses * addressCost + if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + return 0, ErrGasUintOverflow } - gas.RegularGas += storageKeys * storageKeyCost + gas += storageKeys * storageKeyCost } } return gas, nil } -// intrinsicBaseGasEIP2780 computes the regular-gas portion of the EIP-2780 -// intrinsic base cost: the per-resource decomposition of the legacy flat 21,000. +// intrinsicBaseGasEIP2780 computes the intrinsic base cost of the transaction. func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int) uint64 { var ( isContractCreation = to == nil isSelfTransfer = to != nil && *to == from hasValue = value != nil && !value.IsZero() ) - // tx.sender: signature recovery plus the sender account access and write. + // tx.sender: signature recovery, the sender account's access and write, + // and the inclusion of the transaction in the block (which is transient + // and expires with history). gas := params.TxBaseCost2780 - // tx.to charge. + // tx.to charge. Per EIP-2780 the recipient touch is charged at the cold + // rate unconditionally at the intrinsic phase, independent of the account's + // warm/cold state. switch { case isSelfTransfer: // The recipient account is already accessed and written as the sender. case isContractCreation: - gas += params.CreateAccess2780 + gas += params.CreateAccessAmsterdam default: - gas += params.ColdAccountAccess2780 + gas += params.ColdAccountAccessAmsterdam } // tx.value charge. @@ -242,10 +242,12 @@ func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, v tokenCost = params.TxCostFloorPerToken } - // The floor is anchored to the transaction base cost. + // The floor is anchored to the transaction base cost. Under EIP-2780 that + // base is the per-resource decomposition (the same one used by the intrinsic + // gas), so the floor never undercuts the transaction's own base. floorBase := params.TxGas if rules.IsAmsterdam { - floorBase = params.TxBaseCost2780 + floorBase = intrinsicBaseGasEIP2780(from, to, value) } // Check for overflow if (math.MaxUint64-floorBase)/tokenCost < tokens { @@ -260,7 +262,6 @@ func toWordSize(size uint64) uint64 { if size > math.MaxUint64-31 { return math.MaxUint64/32 + 1 } - return (size + 31) / 32 } @@ -423,24 +424,16 @@ func (st *stateTransition) to() common.Address { return *st.msg.To } -// buyGas pre-pays gas from the sender's balance and initializes the -// transaction's gas budget. It is invoked at the tail of preCheck. +// buyGas pre-pays gas from the sender's balance. // // The balance requirement is the worst-case ETH the tx may need to lock // up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`, // plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance -// returns ErrInsufficientFunds. After the check, the sender is actually -// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee` -// under Cancun), the cap-vs-tip differential is settled at tx end. +// returns ErrInsufficientFunds. // -// The gas budget is seeded into both `initialBudget` (frozen snapshot -// for tx-end accounting) and `gasRemaining` (live running balance): -// -// - Pre-Amsterdam: one-dimensional regular budget equal to -// `msg.GasLimit`; the state-gas reservoir is zero. -// - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is -// capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from -// `msg.GasLimit` above that cap becomes the state-gas reservoir. +// After the check, the sender is actually debited `msg.GasLimit × msg.GasPrice` +// (plus `blobGas × blobBaseFee` under Cancun), the cap-vs-tip differential +// is settled at tx end. func (st *stateTransition) buyGas() error { mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) @@ -493,54 +486,63 @@ func (st *stateTransition) buyGas() error { if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } - isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) - - // Reserve the gas budget in the block gas pool - var err error - if isAmsterdam { - err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit) - } else { - err = st.gp.CheckGasLegacy(st.msg.GasLimit) - } - if err != nil { - return err - } - - // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit - limit := st.msg.GasLimit - if isAmsterdam { - limit = min(st.msg.GasLimit, params.MaxTxGas) - } - st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit) - - if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) - } // Deduct the gas cost from the sender's balance st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } +// initRuntimeGasBudget initializes the transaction's running gas budget with the +// gas remaining after the intrinsic cost has been deducted. +// +// After Amsterdam (EIP-8037) the intrinsic cost counts towards the EIP-7825 +// regular-gas cap: +// +// execution_gas = tx.gas - intrinsic_gas +// regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic_gas +// gas_left = min(regular_gas_budget, execution_gas) +// state_gas_reservoir = execution_gas - gas_left +func (st *stateTransition) initRuntimeGasBudget(rules params.Rules, intrinsicGas uint64) { + executionGas := st.msg.GasLimit - intrinsicGas + gasLeft := executionGas + if rules.IsAmsterdam { + gasLeft = min(params.MaxTxGas-intrinsicGas, executionGas) + } + st.gasRemaining = vm.NewGasBudget(gasLeft, executionGas-gasLeft) + + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: st.msg.GasLimit}, st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) + } +} + // preCheck performs all pre-execution validation that does not require -// the EVM to run, then ends by calling buyGas to lock in the gas budget. +// the EVM to run, then ends by calling buyGas to lock ether for prepay. // It returns a consensus error if any of the following fail: // // - Sender nonce matches state and is not at 2^64-1 (EIP-2681). -// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam -// (the cap also bounds the regular dimension after Amsterdam, but -// it is enforced there via the two-dimensional budget in buyGas). +// +// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam. +// // - EIP-3607 sender-is-EOA, allowing accounts whose only code is an // EIP-7702 delegation designator. +// // - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+). +// // - Blob-tx structural checks: non-nil `To`, non-empty hash list, // valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+). +// // - Blob fee-cap not below the current blob base fee (Cancun+). +// // - EIP-7702 set-code-tx shape: non-nil `To` and non-empty // authorization list. // +// - EIP-3860 init code size cap on create transactions (Shanghai+, +// with the raised Amsterdam cap). +// +// - Insufficient block gas budget for including the transaction. +// // The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass // subsets of these checks for simulation paths (eth_call, eth_estimateGas). -func (st *stateTransition) preCheck() error { +func (st *stateTransition) preCheck(rules params.Rules) error { // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { @@ -557,13 +559,9 @@ func (st *stateTransition) preCheck() error { msg.From.Hex(), stNonce) } } - var ( - isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) - isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) - ) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. - if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { + if !rules.IsAmsterdam && rules.IsOsaka && msg.GasLimit > params.MaxTxGas { return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA @@ -574,7 +572,7 @@ func (st *stateTransition) preCheck() error { } } // Make sure that transaction gasFeeCap is greater than the baseFee (post london) - if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { + if rules.IsLondon { // Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { @@ -601,7 +599,7 @@ func (st *stateTransition) preCheck() error { if len(msg.BlobHashes) == 0 { return ErrMissingBlobHashes } - if isOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { + if rules.IsOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { return ErrTooManyBlobs } for i, hash := range msg.BlobHashes { @@ -611,7 +609,7 @@ func (st *stateTransition) preCheck() error { } } // Check that the user is paying at least the current blob fee - if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { + if rules.IsCancun { if st.blobGasUsed() > 0 { // Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() == 0 @@ -634,6 +632,22 @@ func (st *stateTransition) preCheck() error { return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } + // Check whether the init code size has been exceeded (EIP-3860). + if msg.To == nil { + if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { + return err + } + } + // Reserve the gas budget in the block gas pool + var err error + if rules.IsAmsterdam { + err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit) + } else { + err = st.gp.CheckGasLegacy(st.msg.GasLimit) + } + if err != nil { + return err + } return st.buyGas() } @@ -649,32 +663,25 @@ func (st *stateTransition) preCheck() error { // If a consensus error is encountered, it is returned directly with a // nil EVM execution result. func (st *stateTransition) execute() (*ExecutionResult, error) { - // Validate the message and pre-pay gas. - if err := st.preCheck(); err != nil { - return nil, err - } - - // Charge intrinsic gas (with overflow detection inside IntrinsicGas). - // Under Amsterdam the cost is two-dimensional and Charge debits both - // regular and state in one step. var ( msg = st.msg rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) contractCreation = msg.To == nil floorDataGas uint64 ) - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules, st.evm.Context.CostPerStateByte) + // Validate the message and pre-pay gas. + if err := st.preCheck(rules); err != nil { + return nil, err + } + // Calculate the intrinsic gas of this transaction and make sure the gas limit + // is sufficient to cover that. + intrinsicGas, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules) if err != nil { return nil, err } - prior, sufficient := st.gasRemaining.Charge(cost) - if !sufficient { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) + if msg.GasLimit < intrinsicGas { + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, intrinsicGas) } - if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) - } - // Validate the EIP-7623 calldata floor against the gas limit. The floor inflates // the total gas usage at tx end, so the gas limit must be sufficient to cover that. if rules.IsPrague { @@ -687,13 +694,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if msg.GasLimit < floorDataGas { return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) } - // In Amsterdam, the transaction gas limit is allowed to exceed - // params.MaxTxGas, but the calldata floor cost is capped by it. - if rules.IsAmsterdam && max(cost.RegularGas, floorDataGas) > params.MaxTxGas { - return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas) - } + } + // In Amsterdam, the transaction gas limit is allowed to exceed + // params.MaxTxGas, but the intrinsic cost and calldata floor + // cost is still capped by it. + if rules.IsAmsterdam && max(intrinsicGas, floorDataGas) > params.MaxTxGas { + return nil, fmt.Errorf("%w: intrinsic cost %v, floor: %v", ErrFloorDataGas, intrinsicGas, floorDataGas) } + // EIP-4762 setup if rules.IsEIP4762 { st.evm.AccessEvents.AddTxOrigin(msg.From) @@ -718,56 +727,18 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // - enable block-level accessList construction (EIP-7928) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) + // Initialize the running gas budget with the post-intrinsic remainder. + st.initRuntimeGasBudget(rules, intrinsicGas) + // Execute the top-most frame var ( - ret []byte - vmerr error // vm errors do not effect consensus and are therefore not assigned to err - result vm.GasBudget + ret []byte + vmerr error // vm errors do not effect consensus ) if contractCreation { - // Check whether the init code size has been exceeded. - if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { - return nil, err - } - // Execute the transaction's creation. - var creation bool - ret, _, result, creation, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) - - // If the contract creation failed, or the destination was pre-existing, - // refund the account-creation state gas pre-charged in IntrinsicGas. - if rules.IsAmsterdam && !creation { - st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - } + ret, vmerr = st.executeCreate(rules, value) } else { - // Increment the nonce for the next transaction. - st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) - - // Apply EIP-7702 authorizations. - st.applyAuthorizations(rules, msg.SetCodeAuthorizations) - - // Perform convenience warming of sender's delegation target. Although the - // sender is already warmed in Prepare(..), it's possible a delegation to - // the account was deployed during this transaction. To handle correctly, - // simply wait until the final state of delegations is determined before - // performing the resolution and warming. - if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { - st.state.AddAddressToAccessList(addr) - // Record in BAL - if rules.IsAmsterdam { - st.state.GetCode(addr) - } - } - // EIP-2780: charge the transaction's top-level recipient costs. If the - // budget cannot cover the charge, the top frame halts out of gas. - if rules.IsAmsterdam && !st.chargeCallRecipientEIP2780(value) { - vmerr = vm.ErrOutOfGas - st.gasRemaining = st.gasRemaining.ExitHalt() - } else { - // Execute the transaction's call. - ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) - } + ret, vmerr = st.executeCall(rules, value) } // Settle down the gas usage and refund the ETH back if any remaining @@ -808,43 +779,150 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { }, nil } -// chargeCallRecipientEIP2780 applies the EIP-2780 transaction top-level gas costs for -// a message-call transaction, charged before any opcode executes: -// -// - if the recipient is EIP-161 non-existent and the transaction carries value, -// charge for account creation. -// -// - if the recipient is an EIP-7702 delegated account, resolving the delegation -// loads the target's code, charged an additional cold account access in -// regular gas. -func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { - var ( - cost vm.GasCosts - to = *st.msg.To - ) - // This runs in the topmost frame before any bytecode executes, so unlike the - // execution-level checks which must use StateDB.Empty because SELFDESTRUCT can - // leave a transient EIP-161-empty account, no empty account can exist here, and - // !Exist is equivalent to Empty. - if !value.IsZero() && !st.state.Exist(to) { - cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte +// executeCreate runs the top-level frame of a contract-creation transaction +// and returns the EVM return data and the frame-level execution error. +func (st *stateTransition) executeCreate(rules params.Rules, value *uint256.Int) ([]byte, error) { + msg := st.msg + + var chargedCreation bool + if rules.IsAmsterdam { + addr := crypto.CreateAddress(msg.From, st.state.GetNonce(msg.From)) + if st.state.Empty(addr) { + if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) { + // The nonce increment normally performed inside evm.Create + // must still happen for the included transaction. + st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeContractCreator) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + chargedCreation = true + } } - if _, ok := types.ParseDelegation(st.state.GetCode(to)); ok { - // EIP-2780: The tx.sender, tx.to, and (where applicable) delegation-target - // charges above are always at the cold rate. - // - // The delegation-target is already warmed before, no double warming here. - cost.RegularGas += params.ColdAccountAccess2780 + // The first frame is entered with the gas remaining after the runtime + // charges. + ret, _, result, vmerr := st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) + + // If the contract creation failed (e.g. the initcode reverted or halted), + // refill the account-creation state gas charged at runtime. + if rules.IsAmsterdam && chargedCreation && vmerr != nil { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } - if cost == (vm.GasCosts{}) { - return true + // If the top-most frame halted, drain the leftover regular gas rather + // than returning it to the sender. The frame exit itself already burned + // its gas left, but the refill above repays the regular gas the charge + // originally borrowed, and on a halt that repayment must be burned as + // well. The state dimension is left untouched. + if rules.IsAmsterdam && vmerr != nil && vmerr != vm.ErrExecutionReverted { + st.gasRemaining.DrainRegular() } + return ret, vmerr +} + +// executeCall runs the top-level frame of a message-call transaction and +// returns the EVM return data and the frame-level execution error. +func (st *stateTransition) executeCall(rules params.Rules, value *uint256.Int) ([]byte, error) { + msg := st.msg + + // Increment the nonce for the next transaction. + st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) + + if rules.IsAmsterdam { + snapshot := st.state.Snapshot() + if !st.applyAuthorizations(rules, st.msg.SetCodeAuthorizations) { + st.state.RevertToSnapshot(snapshot) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + if !st.chargeCallRecipientEIP2780(value) { + st.state.RevertToSnapshot(snapshot) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + } else { + // Apply EIP-7702 authorizations. + st.applyAuthorizations(rules, msg.SetCodeAuthorizations) + + // Perform convenience warming of sender's delegation target. Although the + // sender is already warmed in Prepare(..), it's possible a delegation to + // the account was deployed during this transaction. To handle correctly, + // simply wait until the final state of delegations is determined before + // performing the resolution and warming. + if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { + st.state.AddAddressToAccessList(addr) + } + } + ret, result, vmerr := st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) + + // If the call frame reverts or halts exceptionally, the charged state-gas + // is refilled back to the state reservoir in Amsterdam. + if rules.IsAmsterdam && vmerr != nil && !value.IsZero() && st.evm.StateDB.Empty(st.to()) { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } + // If the top-most frame halted, drain the leftover regular gas rather + // than returning it to the sender. The frame exit itself already burned + // its gas left, but the refill above repays the regular gas the charge + // originally borrowed, and on a halt that repayment must be burned as + // well. + if rules.IsAmsterdam && vmerr != nil && vmerr != vm.ErrExecutionReverted { + st.gasRemaining.DrainRegular() + } + return ret, vmerr +} + +// chargeRuntimeGas deducts an EIP-2780 runtime charge from the transaction's +// gas budget and reports whether the budget covered it. +func (st *stateTransition) chargeRuntimeGas(cost vm.GasCosts) bool { prior, ok := st.gasRemaining.Charge(cost) if !ok { return false } if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxRuntimeGas) + } + return true +} + +// chargeCallRecipientEIP2780 applies the EIP-2780 runtime charges for the +// top-level recipient of a message-call transaction, before the first frame is +// entered: +// +// - if the recipient is EIP-161 empty and the transaction carries value, +// the durable state growth of the new account; +// +// - if the recipient is an EIP-7702 delegated account, resolving the +// delegation loads the target's code: a cold account access, or a warm +// access if the target is already warm. +// +// Each charge is deducted before the state access it prices is performed: +// under EIP-7928 every account load is recorded in the block access list, so +// an access the budget cannot cover must not happen at all. +func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { + to := *st.msg.To + + // This runs in the topmost frame before any bytecode executes, non-existence + // is equivalent with EIP-161-empty, as no preceding operation can leave a + // transient EIP-161-empty account (such as zero-value transfer). + if !value.IsZero() && st.state.Empty(to) { + if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) { + return false + } + } + if target, delegated := types.ParseDelegation(st.state.GetCode(to)); delegated { + // Pay the delegation-target access before the target is warmed and + // its code resolved (loaded). + cost := vm.GasCosts{RegularGas: params.ColdAccountAccessAmsterdam} + if st.state.AddressInAccessList(target) { + cost.RegularGas = params.WarmAccountAccessAmsterdam + } + if !st.chargeRuntimeGas(cost) { + return false + } + st.state.AddAddressToAccessList(target) + + // Record the delegation in the block level accessList explicitly + st.state.GetCode(target) } return true } @@ -852,27 +930,11 @@ func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { // settleGas finalizes the per-tx gas accounting after EVM execution: // // - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas, -// tx_state_gas) before any refund or floor: -// -// tx_gas_used_before_refund = tx.gas - gas_left - state_gas_reservoir -// tx_state_gas = state_gas_used -// tx_regular_gas = tx_gas_used_before_refund - tx_state_gas -// +// tx_state_gas) before any refund. // - Computes the receipt scalar tx_gas_used by applying the EIP-3529 -// refund (capped at tx_gas_used_before_refund/5) and the EIP-7623 -// calldata floor: -// -// tx_gas_used = max(tx_gas_used_before_refund - tx_gas_refund, calldata_floor) -// +// refund and the EIP-7623 calldata floor. // - Charges the block gas pool (2D under Amsterdam, scalar pre-Amsterdam). -// // - Refunds the leftover gas to the sender as ETH. -// -// Returns the receipt-level tx_gas_used and the pre-refund peak (consumed -// by gas-estimation callers via ExecutionResult.MaxUsedGas). UsedStateGas -// should never become negative in the top-most frame, since state-gas -// refunds occur only when state creation is reverted within the same -// transaction and clearing pre-existing state is never refunded. func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (gasUsed, peakUsed uint64, err error) { if st.gasRemaining.UsedStateGas < 0 { return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas) @@ -881,7 +943,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g // EIP-8037: // tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir - // tx_state_gas = intrinsic_state_gas + tx_output.execution_state_gas_used + // tx_state_gas = tx_output.execution_state_gas_used // tx_regular_gas = max(tx_gas_used_before_refund - tx_state_gas, calldata_floor_gas_cost) gasLeft := st.gasRemaining.RegularGas + st.gasRemaining.StateGas gasUsedBeforeRefund := st.msg.GasLimit - gasLeft @@ -911,6 +973,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g peakUsed = max(peakUsed, floorDataGas) } + // Settle down the final gas consumption in the block-level pool if rules.IsAmsterdam { if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { return 0, 0, err @@ -921,7 +984,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g } } - // Refund leftover gas to the sender as ETH. + // Refund leftover gas to the sender if gasLeft > 0 { refund := new(uint256.Int).Mul(uint256.NewInt(gasLeft), st.msg.GasPrice) st.state.AddBalance(st.msg.From, refund, tracing.BalanceIncreaseGasReturn) @@ -964,51 +1027,71 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } -// applyAuthorization applies an EIP-7702 code delegation to the state and, -// adjust the pre-charged intrinsic cost accordingly. -func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { +// authTracking tracks the charges already paid for an authority by earlier +// authorizations in the same transaction. +type authTracking struct { + written bool // first-write ACCOUNT_WRITE surcharge paid + authBaseCovered bool // indicator exists at tx start, or paid earlier +} + +// applyAuthorization applies an EIP-7702 code delegation to the state. +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, authorities map[common.Address]*authTracking) error { authority, err := st.validateAuthorization(auth) if err != nil { - if rules.IsAmsterdam { - st.gasRemaining.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) - st.state.AddRefund(params.AccountWriteAmsterdam) - } return err } - prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) + oldDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) if !rules.IsAmsterdam { if st.state.Exist(authority) { st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } } else { - if st.state.Exist(authority) { - st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - st.state.AddRefund(params.AccountWriteAmsterdam) - } - authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte + // EIP-2780: charge the state-dependent authorization costs at runtime. + // The authority's cold access was already charged unconditionally at the + // intrinsic phase, so only state-dependent costs remain here. + var cost vm.GasCosts - preDelegated, ok := delegates[authority] - if !ok { - preDelegated = curDelegated - delegates[authority] = preDelegated + track := authorities[authority] + if track == nil { + track = &authTracking{authBaseCovered: curDelegated} + authorities[authority] = track } - if auth.Address == (common.Address{}) { - // Clearing writes no indicator, refill this auth's state charge. - st.gasRemaining.RefundStateToReservoir(authBase) - - // The indicator was created by an earlier auth within the same - // transaction, refill the state charge as it's no longer justified. - if curDelegated && !preDelegated { - st.gasRemaining.RefundStateToReservoir(authBase) - } - } else if curDelegated || preDelegated { - // The 23-byte slot is already occupied, overwriting it writes no - // new bytes, refill the state charge. - st.gasRemaining.RefundStateToReservoir(authBase) + // Every valid authorization writes the authority account: the + // nonce bump, and possibly the delegation indicator. The first + // write to an account within the transaction carries the + // first-write surcharge. At this point the accounts whose write + // has already been paid for are: + // + // - the sender: TX_BASE_COST prices its account write, and the + // gas prepayment and nonce bump have already happened; + // + // - authorities written by preceding valid authorizations in + // this list, which carried the surcharge themselves; + // + // - tx.to, but only when the transaction carries value: + // TX_VALUE_COST prepaid the recipient write at the intrinsic + // phase. A zero-value transaction pays no TX_VALUE_COST, so a + // write to tx.to here is still the first paid write. + hasValue := st.msg.Value != nil && !st.msg.Value.IsZero() + if !track.written && authority != st.msg.From && (authority != st.to() || !hasValue) { + cost.RegularGas += params.AccountWriteAmsterdam + track.written = true + } + // Durable state growth of the new account + if st.state.Empty(authority) { + cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte + } + // Charge the net-new indicator bytes at most once per authority; + // clearing within the same transaction refunds nothing. + if auth.Address != (common.Address{}) && !track.authBaseCovered { + cost.StateGas += params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte + track.authBaseCovered = true + } + if !st.chargeRuntimeGas(cost) { + return ErrOutOfGasRuntime } } - // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) @@ -1020,18 +1103,23 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se return nil } // Install delegation to auth.Address if the delegation changed - if !curDelegated || auth.Address != prevDelegation { + if !curDelegated || auth.Address != oldDelegation { st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) } return nil } -// applyAuthorizations applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { - preDelegated := make(map[common.Address]bool) +// applyAuthorizations applies the EIP-7702 code delegations to the state. +// It reports whether the transaction budget covered all runtime authorization +// charges. +func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) bool { + authorities := make(map[common.Address]*authTracking) for _, auth := range auths { - st.applyAuthorization(rules, &auth, preDelegated) + if err := st.applyAuthorization(rules, &auth, authorities); err == ErrOutOfGasRuntime { + return false + } } + return true } // calcRefund computes the EIP-3529 refund cap against tx_gas_used_before_refund. diff --git a/core/state_transition_test.go b/core/state_transition_test.go index 60edad52e5..ec473fadbc 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -22,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -158,50 +157,50 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860 bool isAmsterdam bool value *uint256.Int - want vm.GasCosts + want uint64 }{ { name: "frontier/empty-call", - want: vm.GasCosts{RegularGas: params.TxGas}, + want: params.TxGas, }, { name: "frontier/contract-creation-pre-homestead", creation: true, isHomestead: false, // pre-homestead, contract creation still uses TxGas - want: vm.GasCosts{RegularGas: params.TxGas}, + want: params.TxGas, }, { name: "homestead/contract-creation", creation: true, isHomestead: true, - want: vm.GasCosts{RegularGas: params.TxGasContractCreation}, + want: params.TxGasContractCreation, }, { name: "frontier/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), // 100 nz bytes * 68 (frontier) - want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasFrontier}, + want: params.TxGas + 100*params.TxDataNonZeroGasFrontier, }, { name: "istanbul/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), isEIP2028: true, // 100 nz bytes * 16 (post-EIP2028) - want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasEIP2028}, + want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028, }, { name: "istanbul/zero-data", data: bytes.Repeat([]byte{0x00}, 100), isEIP2028: true, // 100 zero bytes * 4 - want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas}, + want: params.TxGas + 100*params.TxDataZeroGas, }, { name: "istanbul/mixed-data", data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), isEIP2028: true, - want: vm.GasCosts{RegularGas: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028}, + want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028, }, { name: "shanghai/init-code-word-gas", @@ -211,7 +210,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isEIP3860: true, // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 - want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas}, + want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, }, { name: "shanghai/init-code-non-multiple-of-32", @@ -220,7 +219,7 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isEIP3860: true, - want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas}, + want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas, }, { name: "berlin/access-list", @@ -230,7 +229,7 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, // 2 addrs * 2400 + 3 keys * 1900 - want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas}, + want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas, }, { name: "amsterdam/access-list-extra-cost", @@ -241,10 +240,12 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, // EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess - // (15,000). Plus base access-list charge + EIP-7981 extra. - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + // (15,000); the recipient touch is charged at the cold rate + // unconditionally at the intrinsic phase. Plus base access-list + // charge + EIP-7981 extra. + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + 2*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam + - 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, }, { name: "prague/auth-list", @@ -255,7 +256,7 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple) - want: vm.GasCosts{RegularGas: params.TxGas + 3*params.CallNewAccountGas}, + want: params.TxGas + 3*params.CallNewAccountGas, }, { name: "amsterdam/contract-creation-empty", @@ -263,12 +264,9 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isAmsterdam: true, - // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000), - // and account-creation cost is charged as state gas. - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000); + // the new-account state charge is applied at runtime. + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam, }, { name: "amsterdam/contract-creation-init-code", @@ -278,11 +276,8 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isEIP3860: true, // Shanghai gates init-code word gas isAmsterdam: true, - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + - 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, }, { name: "amsterdam/contract-creation-with-access-list", @@ -295,13 +290,10 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isEIP3860: true, isAmsterdam: true, - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + - 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + - 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + - 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, }, { name: "amsterdam/combined", @@ -314,18 +306,15 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // EIP-8037 splits the auth-tuple charge into regular + state gas, with - // the values finalized by EIP-8038: - // regular: ACCOUNT_WRITE (8,000) + REGULAR_PER_AUTH_BASE_COST (7,500) per auth - // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + - 100*params.TxDataNonZeroGasEIP2028 + - 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + - 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost), - StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, - }, + // EIP-2780: the recipient touch and the per-authorization authority + // access (priced into RegularPerAuthBaseCost) are both charged at the + // cold rate unconditionally at the intrinsic phase; the account leaf + // and indicator bytes are charged at runtime. + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + + 100*params.TxDataNonZeroGasEIP2028 + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + + 1*params.RegularPerAuthBaseCost, }, { name: "amsterdam/value-transfer-call", @@ -333,8 +322,8 @@ func TestIntrinsicGas(t *testing.T) { isAmsterdam: true, value: uint256.NewInt(1), // EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000. - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + - params.TransferLogCost2780 + params.TxValueCost2780}, + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + + params.TransferLogCost2780 + params.TxValueCost2780, }, { name: "amsterdam/value-bearing-contract-creation", @@ -343,11 +332,9 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, value: uint256.NewInt(1), - // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756, plus account-creation state gas. - want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, - }, + // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756; + // the new-account state charge is applied at runtime. + want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780, }, } for _, tt := range tests { @@ -363,7 +350,7 @@ func TestIntrinsicGas(t *testing.T) { to = &addr1 } got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, - common.Address{}, to, tt.value, rules, params.CostPerStateByte) + common.Address{}, to, tt.value, rules) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/core/tracing/gen_gas_change_reason_stringer.go b/core/tracing/gen_gas_change_reason_stringer.go index e40f490051..cf4dbe0d26 100644 --- a/core/tracing/gen_gas_change_reason_stringer.go +++ b/core/tracing/gen_gas_change_reason_stringer.go @@ -29,21 +29,23 @@ func _() { _ = x[GasChangeWitnessContractCollisionCheck-18] _ = x[GasChangeTxDataFloor-19] _ = x[GasChangeRefundAccountCreation-20] + _ = x[GasChangeTxRuntimeGas-21] + _ = x[GasChangeAccountCreation-22] _ = x[GasChangeIgnored-255] } const ( - _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreation" + _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreationTxRuntimeGasAccountCreation" _GasChangeReason_name_1 = "Ignored" ) var ( - _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374} + _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374, 386, 401} ) func (i GasChangeReason) String() string { switch { - case i <= 20: + case i <= 22: return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] case i == 255: return _GasChangeReason_name_1 diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 667c6341b4..ba2428469b 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -476,6 +476,15 @@ const ( // pre-charged account-creation cost when no account is created. GasChangeRefundAccountCreation GasChangeReason = 20 + // GasChangeTxRuntimeGas is the amount of gas charged for the state-dependent + // costs of the transaction per EIP-2780. + GasChangeTxRuntimeGas GasChangeReason = 21 + + // GasChangeAccountCreation represents the conditional account-creation + // state cost charged in the creating frame when a CREATE/CREATE2 is about + // to create a new account (EIP-8037). + GasChangeAccountCreation GasChangeReason = 22 + // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. GasChangeIgnored GasChangeReason = 0xFF diff --git a/core/txpool/validation.go b/core/txpool/validation.go index b53e3ee2bf..27cbb84ad1 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -132,12 +132,12 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), from, tx.To(), value, rules, params.CostPerStateByte) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), from, tx.To(), value, rules) if err != nil { return err } - if tx.Gas() < intrGas.RegularGas { - return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas.RegularGas) + if tx.Gas() < intrGas { + return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas) } // Ensure the transaction can cover floor data gas. if rules.IsPrague { @@ -152,8 +152,8 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // In Amsterdam, the transaction gas limit is allowed to exceed // params.MaxTxGas, but the calldata floor cost is capped by it. - if rules.IsAmsterdam && max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas { - return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas) + if rules.IsAmsterdam && max(intrGas, floorDataGas) > params.MaxTxGas { + return fmt.Errorf("%w: intrinsic cost %v, floor: %v", core.ErrFloorDataGas, intrGas, floorDataGas) } } // Ensure the gasprice is high enough to cover the requirement of the calling pool diff --git a/core/vm/common.go b/core/vm/common.go index 5059b4af37..46be1a813a 100644 --- a/core/vm/common.go +++ b/core/vm/common.go @@ -36,7 +36,6 @@ func CheckMaxInitCodeSize(rules *params.Rules, size uint64) error { return fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, size, params.MaxInitCodeSize) } } - return nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index 15609a0205..591663a8d9 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -473,20 +473,57 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b return ret, exitGas, err } -// create creates a new contract using code as deployment code. -func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, creation bool, err error) { - // Depth check execution. Fail if we're trying to execute above the - // limit. - var nonce uint64 +// createFramePreCheck the precondition before executing the contract deployment, +// halts the create frame if fails with any check below. +func (evm *EVM) createFramePreCheck(caller common.Address, value *uint256.Int) error { if evm.depth > int(params.CallCreateDepth) { - err = ErrDepth - } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) { - err = ErrInsufficientBalance - } else { - nonce = evm.StateDB.GetNonce(caller) - if nonce+1 < nonce { - err = ErrNonceUintOverflow - } + return ErrDepth + } + if !evm.Context.CanTransfer(evm.StateDB, caller, value) { + return ErrInsufficientBalance + } + nonce := evm.StateDB.GetNonce(caller) + if nonce+1 < nonce { + return ErrNonceUintOverflow + } + return nil +} + +// chargeAccountCreation runs the create-frame precheck and charges the +// account-creation state gas since Amsterdam, before the 63/64ths split. +// +// The charge only applies if the destination is empty, skipping pre-funded +// deployment destinations. Note, a destination colliding on storage alone +// (zero nonce, zero balance, empty code) is still empty and is charged. +// +// If halt is true, the caller must terminate with the returned error: +// - a failed precheck halts the create frame only and parent frame continues, +// - an insufficient charge halts the parent frame with ErrOutOfGas. +func (evm *EVM) chargeAccountCreation(scope *ScopeContext, contractAddr common.Address, value *uint256.Int) (charged, halt bool, err error) { + if !evm.chainRules.IsAmsterdam { + return false, false, nil + } + if err := evm.createFramePreCheck(scope.Contract.Address(), value); err != nil { + scope.Stack.get().Clear() + evm.returnData = nil + return false, true, nil + } + if !evm.StateDB.Empty(contractAddr) { + return false, false, nil + } + cost := params.AccountCreationSize * evm.Context.CostPerStateByte + if !scope.Contract.chargeState(cost, evm.Config.Tracer, tracing.GasChangeAccountCreation) { + return false, true, ErrOutOfGas + } + return true, false, nil +} + +// create creates a new contract using code as deployment code. +func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) { + // Since Amsterdam, the precheck has been folded into the parent frame + // due to account-creation determination, so skip the duplicate check here. + if !evm.chainRules.IsAmsterdam { + err = evm.createFramePreCheck(caller, value) } if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) @@ -495,17 +532,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value }(gas) } if err != nil { - return nil, common.Address{}, gas, false, err + return nil, common.Address{}, gas, err } // Increment the caller's nonce after passing all validations - evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) + evm.StateDB.SetNonce(caller, evm.StateDB.GetNonce(caller)+1, tracing.NonceChangeContractCreator) // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) @@ -532,7 +569,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } // EIP-8037 collision rule: the state reservoir is fully preserved on // address collision while regular gas is burnt. - return nil, common.Address{}, halt, false, ErrContractAddressCollision + return nil, common.Address{}, halt, ErrContractAddressCollision } // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent @@ -540,7 +577,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value snapshot := evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) - creation = true } // CreateContract means that regardless of whether the account previously existed // in the state trie or not, it _now_ becomes created as a _contract_ account. @@ -555,7 +591,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -586,11 +622,11 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) } } - return ret, address, exit, false, err + return ret, address, exit, err } // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved). // Both packaged as a success-form GasBudget. - return ret, address, contract.Gas.ExitSuccess(), creation, err + return ret, address, contract.Gas.ExitSuccess(), err } // initNewContract runs a new contract's creation code, performs checks on the @@ -646,7 +682,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } // Create creates a new contract using code as deployment code. -func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) { +func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller)) return evm.create(caller, code, gas, value, contractAddr, CREATE) } @@ -655,7 +691,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. -func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) { +func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { inithash := crypto.Keccak256Hash(code) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index d8fbf2b461..4f5d4979fd 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -560,12 +560,10 @@ func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m words := (size + 31) / 32 wordGas := params.InitCodeWordGas * words - // Unconditionally pre-charge the account creation and refunds if the creation - // doesn't happen after the create-frame. - return GasCosts{ - RegularGas: gas + wordGas, - StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte, - }, nil + // The account-creation state gas is not part of the opcode cost: it is + // charged conditionally at the destination access, in the creating frame, + // right before the 63/64ths split (see opCreate). + return GasCosts{RegularGas: gas + wordGas}, nil } func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -590,12 +588,10 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, // (for address hashing). wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words - // Unconditionally pre-charge the account creation and refunds if the creation - // doesn't happen after the create-frame. - return GasCosts{ - RegularGas: gas + wordGas, - StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte, - }, nil + // The account-creation state gas is not part of the opcode cost: it is + // charged conditionally at the destination access, in the creating frame, + // right before the 63/64ths split (see opCreate2). + return GasCosts{RegularGas: gas + wordGas}, nil } // regularGasCall8038 is the intrinsic regular-gas calculator for CALL in @@ -635,19 +631,13 @@ func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error transfersValue = !stack.back(2).IsZero() address = common.Address(stack.back(1).Bytes20()) ) - // TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated? - // It's technically possible to skip the EIP158 but very unlikely in practice. - if evm.chainRules.IsEIP158 { - // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist - // in the current state yet still be considered non-existent by EIP-161 if its - // nonce, balance, and code are all zero. Such accounts can appear temporarily - // during execution (e.g. via SELFDESTRUCT) and are removed at tx end. - // - // Funding such an account makes it permanent state growth and must be charged. - if transfersValue && evm.StateDB.Empty(address) { - gas += params.AccountCreationSize * evm.Context.CostPerStateByte - } - } else if !evm.StateDB.Exist(address) { + // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist + // in the current state yet still be considered non-existent by EIP-161 if its + // nonce, balance, and code are all zero. Such accounts can appear temporarily + // during execution (e.g. via SELFDESTRUCT) and are removed at tx end. + // + // Funding such an account makes it permanent state growth and must be charged. + if transfersValue && evm.StateDB.Empty(address) { gas += params.AccountCreationSize * evm.Context.CostPerStateByte } return gas, nil @@ -698,22 +688,26 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor var ( y, x = stack.back(1), stack.peek() slot = common.Hash(x.Bytes32()) - value = common.Hash(y.Bytes32()) stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte ) // Check slot presence in the access list - access := params.WarmStorageReadCostEIP2929 - if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + access := params.WarmStorageAccessAmsterdam + _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot) + if !slotPresent { access = params.ColdStorageAccessAmsterdam - evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } // Check access cost affordability before reading slot if contract.Gas.RegularGas < access { return GasCosts{}, errors.New("not enough gas for slot access") } + if !slotPresent { + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + } // Read the slot value for gas cost measurement - current, original := evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) - + var ( + value = common.Hash(y.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + ) if current == value { // noop (1) return GasCosts{RegularGas: access}, nil } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 76a2c7af60..220e7d650f 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -48,18 +48,6 @@ func (g GasCosts) String() string { // - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross // consumption. UsedStateGas is signed so it can be decremented by inline // state-gas refunds (e.g., SSTORE 0->A->0). -// -// The same struct serves three roles: -// -// - During execution: Charge / ChargeRegular / ChargeState / RefundState -// and RefundRegular mutate the running balance and the usage accumulators -// in lockstep. -// -// - At frame exit: ExitSuccess / ExitRevert / ExitHalt produce a new -// GasBudget in "leftover" form that packages the result for the caller. -// -// - At absorption: the caller's Absorb method merges the child's leftover -// budget into its own running budget. type GasBudget struct { RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) @@ -78,9 +66,7 @@ func NewGasBudget(regular, state uint64) GasBudget { return GasBudget{RegularGas: regular, StateGas: state} } -// Used returns the total scalar gas consumed relative to an initial budget -// (= (initial.regular + initial.state) − (current.regular + current.state)). -// This is the payment scalar (EIP-8037's tx_gas_used_before_refund). +// Used returns the total scalar gas consumed relative to an initial budget. func (g GasBudget) Used(initial GasBudget) uint64 { return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) } @@ -91,16 +77,16 @@ func (g GasBudget) String() string { } // Charge deducts a combined regular+state cost from the running balance and -// updates the usage accumulators. State-gas in excess of the reservoir spills -// into regular_gas. +// updates the usage accumulators. func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { prior := *g ok := g.charge(cost) return prior, ok } -// chargeRegularOnly deducts a regular-only cost. -func (g *GasBudget) chargeRegularOnly(r uint64) bool { +// ChargeRegularOnly deducts a regular-only cost. It's always preferred for +// performance consideration if the opcode doesn't have any state cost. +func (g *GasBudget) ChargeRegularOnly(r uint64) bool { if g.RegularGas < r { return false } @@ -110,9 +96,7 @@ func (g *GasBudget) chargeRegularOnly(r uint64) bool { } // CanAfford reports whether the running budget can cover the given cost vector -// without going out of gas. The regular cost must fit in the regular balance, -// and any state gas in excess of the reservoir must be coverable by the -// remaining regular gas (the spillover), mirroring charge without mutating. +// without going out of gas. func (g GasBudget) CanAfford(cost GasCosts) bool { if g.RegularGas < cost.RegularGas { return false @@ -162,8 +146,7 @@ func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) { return g.Charge(GasCosts{RegularGas: r}) } -// ChargeState is a convenience that deducts a state-only cost (spills to -// regular when the reservoir is exhausted). Returns false on OOG. +// ChargeState is a convenience that deducts a state-only cost. func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) { return g.Charge(GasCosts{StateGas: s}) } @@ -174,56 +157,24 @@ func (g *GasBudget) IsZero() bool { } // RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0). -// -// Per EIP-8037, the refund repays the regular gas previously borrowed for -// state-gas spillover (tracked by Spilled) before crediting the -// reservoir: it is returned to RegularGas up to the outstanding borrowed -// amount, and only the remainder tops up StateGas. -// -// The signed usage counter is decremented by the full refund regardless of the -// split, preserving the per-frame invariant: -// -// StateGas + UsedStateGas == initialStateGas + Spilled -// -// which the revert and halt paths rely on for the correct gross refund. func (g *GasBudget) RefundState(s uint64) { repay := min(s, g.Spilled) g.RegularGas += repay g.Spilled -= repay - - // Whatever is left tops up the reservoir. g.StateGas += s - repay g.UsedStateGas -= int64(s) } -// RefundStateToReservoir credits a state-gas refund directly to the -// reservoir, without repaying spilled regular gas first. -// -// Per the spec's set_delegation, authorization refunds (and the post-create -// new-account refund) are added to message.state_gas_reservoir directly, in -// contrast to the LIFO inline refunds handled by RefundState. The usage -// counter is decremented by the full amount, matching the spec's -// tx_state_gas = intrinsic_state + state_gas_used - state_refund and -// preserving the per-frame invariant: -// -// StateGas + UsedStateGas == initialStateGas + Spilled -func (g *GasBudget) RefundStateToReservoir(s uint64) { - g.StateGas += s - g.UsedStateGas -= int64(s) +// DrainRegular burns the remaining regular-gas. +func (g *GasBudget) DrainRegular() { + g.UsedRegularGas += g.RegularGas + g.RegularGas = 0 } // Forward drains `regular` regular gas and the entire state reservoir from // the parent's running budget and returns the initial GasBudget for a child // frame. The parent's UsedRegularGas is bumped by the forwarded amount so // that the absorb-on-return path correctly reclaims the unused portion. -// -// Used by frame boundaries where the regular forward has NOT been pre- -// deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The -// CALL family pre-deducts the forward via the dynamic gas table for tracer- -// reporting reasons and therefore constructs its child budget directly. -// -// Caller must ensure `regular` does not exceed the running balance and -// apply any EIP-150 1/64 retention before calling Forward. func (g *GasBudget) Forward(regular uint64) GasBudget { g.RegularGas -= regular g.UsedRegularGas += regular @@ -249,19 +200,15 @@ func (g *GasBudget) ForwardAll() GasBudget { // absorb to update its own state. // ============================================================================ -// ExitSuccess produces the leftover form for a successful frame. Inline -// state-gas refunds have already been folded into StateGas / UsedStateGas -// during execution; the running budget IS the exit budget on success. +// ExitSuccess produces the leftover form for a successful frame. func (g GasBudget) ExitSuccess() GasBudget { return g } // ExitRevert produces the leftover for a REVERT exit. The frame's state -// changes are discarded, so all state gas it charged is refilled to its origin -// (EIP-8037): up to Spilled is returned to RegularGas (the regular -// gas it borrowed), and the remainder restores the reservoir. Because the -// borrowed regular gas is repaid first, the reservoir is made whole back to its -// start-of-frame value. +// changes are discarded, so all state gas it charged is refilled with LIFO +// mechanism: up to Spilled is returned to RegularGas (the regular gas it +// borrowed), and the remainder restores the reservoir. func (g GasBudget) ExitRevert() GasBudget { reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled) if reservoir < 0 { @@ -280,10 +227,10 @@ func (g GasBudget) ExitRevert() GasBudget { } // ExitHalt produces the leftover for an exceptional halt. As with a revert, the -// frame's state changes are rolled back and its state gas is refilled to origin -// (EIP-8037); the difference is that the frame's gas_left is consumed rather +// frame's state changes are rolled back and its state gas is refilled with LIFO +// mechanism. The difference is that the frame's regular gas is consumed rather // than returned. The portion refilled to RegularGas is therefore burned along -// with the rest of gas_left, leaving only the reservoir portion to survive, +// with the rest of regular gas, leaving only the reservoir portion to survive, // which equals the reservoir's value at the start of the frame. func (g GasBudget) ExitHalt() GasBudget { reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 328623848e..2cc8defb84 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -635,7 +635,12 @@ 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()) + contractAddr = crypto.CreateAddress(scope.Contract.Address(), evm.StateDB.GetNonce(scope.Contract.Address())) ) + creationCharged, halt, err := evm.chargeAccountCreation(scope, contractAddr, &value) + if halt { + return nil, err + } // Apply EIP-150 to the regular gas left after the state charge. forward := scope.Contract.Gas.RegularGas if evm.chainRules.IsEIP150 { @@ -646,7 +651,8 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue := size child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation) - res, addr, result, creation, suberr := evm.Create(scope.Contract.Address(), input, child, &value) + res, addr, result, suberr := evm.create(scope.Contract.Address(), input, child, &value, contractAddr, CREATE) + // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -663,8 +669,11 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Refund the leftover gas back to current frame scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - // Refund the state gas of account-creation if creation doesn't happen - if evm.GetRules().IsAmsterdam && !creation { + // Refill the account-creation charge if the create frame failed (reverted, + // halted exceptionally, or collided); a successful creation consumes it. + // This rule is only applied since the Amsterdam, therefore all non-nil vm + // error can be interpreted as deployment failure. + if creationCharged && suberr != nil { scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation) } if suberr == ErrExecutionReverted { @@ -681,7 +690,13 @@ 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()) + inithash = crypto.Keccak256Hash(input) + contractAddr = crypto.CreateAddress2(scope.Contract.Address(), salt.Bytes32(), inithash[:]) ) + creationCharged, halt, err := evm.chargeAccountCreation(scope, contractAddr, &endowment) + if halt { + return nil, err + } // Apply EIP-150 to the regular gas left after the state charge. forward := scope.Contract.Gas.RegularGas forward -= forward / 64 @@ -689,7 +704,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // reuse size int for stackvalue stackvalue := size child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) - res, addr, result, creation, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt) + res, addr, result, suberr := evm.create(scope.Contract.Address(), input, child, &endowment, contractAddr, CREATE2) // Push item on the stack based on the returned error. if suberr != nil { stackvalue.Clear() @@ -701,8 +716,11 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Refund the leftover gas back to current frame scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - // Refund the state gas of account-creation if creation doesn't happen - if evm.GetRules().IsAmsterdam && !creation { + // Refill the account-creation charge if the create frame failed (reverted, + // halted exceptionally, or collided); a successful creation consumes it. + // This rule is only applied since the Amsterdam, therefore all non-nil vm + // error can be interpreted as deployment failure. + if creationCharged && suberr != nil { scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation) } if suberr == ErrExecutionReverted { diff --git a/core/vm/interface.go b/core/vm/interface.go index 5bba39069c..4adee2451f 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -77,9 +77,11 @@ type StateDB interface { AddressInAccessList(addr common.Address) bool SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddAddressToAccessList(addr common.Address) + // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddSlotToAccessList(addr common.Address, slot common.Hash) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index df69c3d1fd..c2dfe3769c 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -192,7 +192,7 @@ 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.chargeRegularOnly(cost) { + if !contract.Gas.ChargeRegularOnly(cost) { return nil, ErrOutOfGas } @@ -223,7 +223,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) } if dynamicCost.StateGas == 0 { - if !contract.Gas.chargeRegularOnly(dynamicCost.RegularGas) { + if !contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas) { return nil, ErrOutOfGas } } else if !contract.Gas.charge(dynamicCost) { diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 3669626d5a..8221e02663 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -493,7 +493,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat // EIP-7702 delegation check. if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { if evm.StateDB.AddressInAccessList(target) { - eip7702Cost = params.WarmStorageReadCostEIP2929 + eip7702Cost = params.WarmAccountAccessAmsterdam } else { evm.StateDB.AddAddressToAccessList(target) eip7702Cost = coldCost diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 8dfa0fb740..f7554fbfd8 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -187,7 +187,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { limit = min(cfg.GasLimit, params.MaxTxGas) } // Call the code with the given configuration. - code, address, result, _, err := vmenv.Create( + code, address, result, err := vmenv.Create( cfg.Origin, input, vm.NewGasBudget(limit, cfg.GasLimit-limit), diff --git a/params/protocol_params.go b/params/protocol_params.go index f6f71e0dc0..7928972a51 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -31,8 +31,6 @@ const ( MaxTxGas uint64 = 1 << 24 // Maximum transaction gas limit after eip-7825 (16,777,216). MaximumExtraDataSize uint64 = 32 // Maximum size extra data may be after Genesis. - ExpByteGas uint64 = 10 // Times ceil(log256(exponent)) for the EXP instruction. - SloadGas uint64 = 50 // CallValueTransferGas uint64 = 9000 // Paid for CALL when the value transfer is non-zero. CallNewAccountGas uint64 = 25000 // Paid for CALL when the destination address didn't exist prior. TxGas uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions. @@ -75,8 +73,7 @@ const ( // Which becomes: 5000 - 2100 + 1900 = 4800 SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas - JumpdestGas uint64 = 1 // Once per JUMPDEST operation. - EpochDuration uint64 = 30000 // Duration between proof-of-work epochs. + JumpdestGas uint64 = 1 // Once per JUMPDEST operation. CreateDataGas uint64 = 200 // CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack. @@ -84,7 +81,6 @@ const ( LogGas uint64 = 375 // Per LOG* operation. CopyGas uint64 = 3 // Multiplied by the number of 32-byte words that are copied (round up) for any *COPY operation and added. StackLimit uint64 = 1024 // Maximum size of VM stack allowed. - TierStepGas uint64 = 0 // Once per operation, for a selection of them. LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction. Create2Gas uint64 = 32000 // Once per CREATE2 operation @@ -101,20 +97,27 @@ const ( TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 - RegularPerAuthBaseCost uint64 = 7816 // As defined by EIP-8037 and EIP-8038 + // RegularPerAuthBaseCost is the state-independent per-authorization floor, + // defined in EIP-8037 as the sum of: + // + // - Calldata cost for the authorization tuple + // - ECDSA recovery of the authority address + // - Cold authority access (COLD_ACCOUNT_ACCESS) + // - Warm writes to the authority account + RegularPerAuthBaseCost uint64 = 7816 // EIP-2780: resource-based intrinsic transaction gas. - TxBaseCost2780 uint64 = 12000 - ColdAccountAccess2780 uint64 = 3000 - CreateAccess2780 uint64 = 11000 - TxValueCost2780 uint64 = 4244 - TransferLogCost2780 uint64 = 1756 + TxBaseCost2780 uint64 = 12000 + TxValueCost2780 uint64 = 4244 + TransferLogCost2780 uint64 = 1756 // EIP-8038: state-access gas cost update (Amsterdam). ColdAccountAccessAmsterdam uint64 = 3000 // COLD_ACCOUNT_ACCESS: cold touch of an account + WarmAccountAccessAmsterdam uint64 = 100 // WARM_ACCESS: warm touch of an account AccountWriteAmsterdam uint64 = 8000 // ACCOUNT_WRITE: surcharge for first-time write to an account CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300) ColdStorageAccessAmsterdam uint64 = 3000 // COLD_STORAGE_ACCESS: cold touch of a storage slot + WarmStorageAccessAmsterdam uint64 = 100 // WARM_STORAGE_ACCESS: warm touch of a storage slot StorageWriteAmsterdam uint64 = 10000 // STORAGE_WRITE: surcharge for first-time write to a storage slot StorageClearRefundAmsterdam uint64 = 12480 // STORAGE_CLEAR_REFUND: refund for clearing a storage slot CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS diff --git a/tests/block_test.go b/tests/block_test.go index 8c1a7ace0f..a054e59e0f 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -94,7 +94,6 @@ func TestExecutionSpecBlocktests(t *testing.T) { // Broken tests bt.skipLoad(`.*eip7610_create_collision/initcollision/.*`) bt.skipLoad(`.*eip7610_create_collision/revert_in_create/.*`) - bt.skipLoad(`.*stRandom2/random_statetest642/.*`) bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { execBlockTest(t, bt, test) diff --git a/tests/gen_sttransaction.go b/tests/gen_sttransaction.go index b25ce76166..31ff38bede 100644 --- a/tests/gen_sttransaction.go +++ b/tests/gen_sttransaction.go @@ -20,7 +20,7 @@ func (s stTransaction) MarshalJSON() ([]byte, error) { GasPrice *math.HexOrDecimal256 `json:"gasPrice"` MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"` MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"` - Nonce math.HexOrDecimal64 `json:"nonce"` + Nonce *math.HexOrDecimal256 `json:"nonce"` To string `json:"to"` Data []string `json:"data"` AccessLists []*types.AccessList `json:"accessLists,omitempty"` @@ -36,7 +36,7 @@ func (s stTransaction) MarshalJSON() ([]byte, error) { enc.GasPrice = (*math.HexOrDecimal256)(s.GasPrice) enc.MaxFeePerGas = (*math.HexOrDecimal256)(s.MaxFeePerGas) enc.MaxPriorityFeePerGas = (*math.HexOrDecimal256)(s.MaxPriorityFeePerGas) - enc.Nonce = math.HexOrDecimal64(s.Nonce) + enc.Nonce = (*math.HexOrDecimal256)(s.Nonce) enc.To = s.To enc.Data = s.Data enc.AccessLists = s.AccessLists @@ -61,7 +61,7 @@ func (s *stTransaction) UnmarshalJSON(input []byte) error { GasPrice *math.HexOrDecimal256 `json:"gasPrice"` MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"` MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"` - Nonce *math.HexOrDecimal64 `json:"nonce"` + Nonce *math.HexOrDecimal256 `json:"nonce"` To *string `json:"to"` Data []string `json:"data"` AccessLists []*types.AccessList `json:"accessLists,omitempty"` @@ -87,7 +87,7 @@ func (s *stTransaction) UnmarshalJSON(input []byte) error { s.MaxPriorityFeePerGas = (*big.Int)(dec.MaxPriorityFeePerGas) } if dec.Nonce != nil { - s.Nonce = uint64(*dec.Nonce) + s.Nonce = (*big.Int)(dec.Nonce) } if dec.To != nil { s.To = *dec.To diff --git a/tests/state_test_util.go b/tests/state_test_util.go index c41e680463..6e1fd1b634 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -116,7 +116,7 @@ type stTransaction struct { GasPrice *big.Int `json:"gasPrice"` MaxFeePerGas *big.Int `json:"maxFeePerGas"` MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"` - Nonce uint64 `json:"nonce"` + Nonce *big.Int `json:"nonce"` To string `json:"to"` Data []string `json:"data"` AccessLists []*types.AccessList `json:"accessLists,omitempty"` @@ -133,7 +133,7 @@ type stTransactionMarshaling struct { GasPrice *math.HexOrDecimal256 MaxFeePerGas *math.HexOrDecimal256 MaxPriorityFeePerGas *math.HexOrDecimal256 - Nonce math.HexOrDecimal64 + Nonce *math.HexOrDecimal256 GasLimit []math.HexOrDecimal64 PrivateKey hexutil.Bytes BlobGasFeeCap *math.HexOrDecimal256 @@ -392,6 +392,16 @@ func (t *StateTest) genesis(config *params.ChainConfig) *core.Genesis { } func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Message, error) { + // The nonce is parsed as an arbitrary-precision integer so that fixtures + // probing the EIP-2681 limit can be loaded; such a transaction can never + // be RLP-decoded and must be rejected here. + var nonce uint64 + if tx.Nonce != nil { + if !tx.Nonce.IsUint64() { + return nil, fmt.Errorf("nonce %v exceeds 2^64-1 (EIP-2681)", tx.Nonce) + } + nonce = tx.Nonce.Uint64() + } var from common.Address // If 'sender' field is present, use that if tx.Sender != nil { @@ -481,7 +491,7 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess msg := &core.Message{ From: from, To: to, - Nonce: tx.Nonce, + Nonce: nonce, Value: uint256.MustFromBig(value), GasLimit: gasLimit, GasPrice: uint256.MustFromBig(gasPrice), diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 010c31324b..9dc9dc42f1 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -86,11 +86,11 @@ func (tt *TransactionTest) Run() error { if overflow { return sender, hash, 0, errors.New("value exceeds 256 bits") } - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), sender, tx.To(), value, rules, params.CostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), sender, tx.To(), value, rules) if err != nil { return } - requiredGas = cost.RegularGas + requiredGas = cost if requiredGas > tx.Gas() { return sender, hash, 0, fmt.Errorf("insufficient gas ( %d < %d )", tx.Gas(), requiredGas) } @@ -125,6 +125,8 @@ func (tt *TransactionTest) Run() error { {"Shanghai", true}, {"Cancun", true}, {"Prague", true}, + {"Osaka", true}, + {"Amsterdam", true}, } { expected := tt.Result[testcase.name] if expected == nil { From 0d1cf34ec66928a2f64ba0ae72f7b272aa2458c0 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:37:29 -0600 Subject: [PATCH 20/20] all: add bogota fork to config (#34057) Adds stubs for Bogota fork. --- core/vm/contracts.go | 4 ++++ core/vm/evm.go | 2 ++ core/vm/jump_table.go | 6 ++++++ core/vm/jump_table_export.go | 2 ++ eth/catalyst/witness.go | 6 +++--- params/config.go | 37 +++++++++++++++++++++++++++++++++--- params/forks/forks.go | 2 ++ 7 files changed, 53 insertions(+), 6 deletions(-) diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 6908ffeba1..7cfe63be2a 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -215,6 +215,8 @@ func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { switch { case rules.IsUBT: return PrecompiledContractsVerkle + case rules.IsBogota: + return PrecompiledContractsOsaka case rules.IsOsaka: return PrecompiledContractsOsaka case rules.IsPrague: @@ -240,6 +242,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsBogota: + return PrecompiledAddressesOsaka case rules.IsOsaka: return PrecompiledAddressesOsaka case rules.IsPrague: diff --git a/core/vm/evm.go b/core/vm/evm.go index 591663a8d9..18aa50a8c8 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -150,6 +150,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { + case evm.chainRules.IsBogota: + evm.table = &bogotaInstructionSet case evm.chainRules.IsAmsterdam: evm.table = &amsterdamInstructionSet case evm.chainRules.IsOsaka: diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 6b931083fe..d92b42aaa5 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -68,6 +68,7 @@ var ( pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() amsterdamInstructionSet = newAmsterdamInstructionSet() + bogotaInstructionSet = newBogotaInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -91,6 +92,11 @@ func validate(jt JumpTable) JumpTable { return jt } +func newBogotaInstructionSet() JumpTable { + instructionSet := newOsakaInstructionSet() + return validate(instructionSet) +} + func newVerkleInstructionSet() JumpTable { instructionSet := newShanghaiInstructionSet() enable4762(&instructionSet) diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index a4a99ea498..2c4ce0ae8b 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) { switch { case rules.IsUBT: return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") + case rules.IsBogota: + return newBogotaInstructionSet(), nil case rules.IsAmsterdam: return newAmsterdamInstructionSet(), nil case rules.IsOsaka: diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index 05bcbbd81f..c0137ae8cc 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -74,7 +74,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(ctx context.Context, upd return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: return engine.STATUS_INVALID, attributesErr("missing beacon root") - case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5, forks.Bogota): return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka payloads") } } @@ -152,7 +152,7 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV4(ctx context.Context, params eng return invalidStatus, paramsErr("nil beaconRoot post-cancun") case executionRequests == nil: return invalidStatus, paramsErr("nil executionRequests post-prague") - case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5, forks.Bogota): return invalidStatus, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads") } requests := convertRequests(executionRequests) @@ -259,7 +259,7 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV4(params engine.ExecutableData, return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun") case executionRequests == nil: return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague") - case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5, forks.Bogota): return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads") } requests := convertRequests(executionRequests) diff --git a/params/config.go b/params/config.go index 7b4493609f..85c3ae401f 100644 --- a/params/config.go +++ b/params/config.go @@ -64,6 +64,7 @@ var ( OsakaTime: newUint64(1764798551), BPO1Time: newUint64(1765290071), BPO2Time: newUint64(1767747671), + BogotaTime: nil, DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"), Ethash: new(EthashConfig), BlobScheduleConfig: &BlobScheduleConfig{ @@ -99,6 +100,7 @@ var ( OsakaTime: newUint64(1759308480), BPO1Time: newUint64(1759800000), BPO2Time: newUint64(1760389824), + BogotaTime: nil, DepositContractAddress: common.HexToAddress("0x4242424242424242424242424242424242424242"), Ethash: new(EthashConfig), BlobScheduleConfig: &BlobScheduleConfig{ @@ -134,6 +136,7 @@ var ( OsakaTime: newUint64(1760427360), BPO1Time: newUint64(1761017184), BPO2Time: newUint64(1761607008), + BogotaTime: nil, DepositContractAddress: common.HexToAddress("0x7f02c3e3c98b133055b8b348b2ac625669ed295d"), Ethash: new(EthashConfig), BlobScheduleConfig: &BlobScheduleConfig{ @@ -169,6 +172,7 @@ var ( OsakaTime: newUint64(1761677592), BPO1Time: newUint64(1762365720), BPO2Time: newUint64(1762955544), + BogotaTime: nil, DepositContractAddress: common.HexToAddress("0x00000000219ab540356cBB839Cbe05303d7705Fa"), Ethash: new(EthashConfig), BlobScheduleConfig: &BlobScheduleConfig{ @@ -203,6 +207,7 @@ var ( CancunTime: nil, PragueTime: nil, OsakaTime: nil, + BogotaTime: nil, UBTTime: nil, Ethash: new(EthashConfig), Clique: nil, @@ -228,6 +233,7 @@ var ( TerminalTotalDifficulty: big.NewInt(0), PragueTime: newUint64(0), OsakaTime: newUint64(0), + BogotaTime: newUint64(0), BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, @@ -258,6 +264,7 @@ var ( CancunTime: nil, PragueTime: nil, OsakaTime: nil, + BogotaTime: nil, UBTTime: nil, TerminalTotalDifficulty: big.NewInt(math.MaxInt64), Ethash: nil, @@ -288,6 +295,7 @@ var ( CancunTime: nil, PragueTime: nil, OsakaTime: nil, + BogotaTime: nil, UBTTime: nil, TerminalTotalDifficulty: big.NewInt(math.MaxInt64), Ethash: new(EthashConfig), @@ -318,6 +326,7 @@ var ( CancunTime: newUint64(0), PragueTime: newUint64(0), OsakaTime: newUint64(0), + BogotaTime: nil, UBTTime: nil, TerminalTotalDifficulty: big.NewInt(0), Ethash: new(EthashConfig), @@ -352,6 +361,7 @@ var ( CancunTime: nil, PragueTime: nil, OsakaTime: nil, + BogotaTime: nil, UBTTime: nil, TerminalTotalDifficulty: big.NewInt(math.MaxInt64), Ethash: new(EthashConfig), @@ -453,6 +463,7 @@ type ChainConfig struct { BPO4Time *uint64 `json:"bpo4Time,omitempty"` // BPO4 switch time (nil = no fork, 0 = already on bpo4) BPO5Time *uint64 `json:"bpo5Time,omitempty"` // BPO5 switch time (nil = no fork, 0 = already on bpo5) AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` // Amsterdam switch time (nil = no fork, 0 = already on amsterdam) + BogotaTime *uint64 `json:"bogotaTime,omitempty"` // Bogota switch time (nil = no fork, 0 = already on bogota) UBTTime *uint64 `json:"ubtTime,omitempty"` // UBT switch time (nil = no fork, 0 = already on UBT) // TerminalTotalDifficulty is the amount of total difficulty reached by @@ -582,6 +593,9 @@ func (c *ChainConfig) String() string { if c.AmsterdamTime != nil { result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime) } + if c.BogotaTime != nil { + result += fmt.Sprintf(", BogotaTime: %v", *c.BogotaTime) + } if c.UBTTime != nil { result += fmt.Sprintf(", UBTTime: %v", *c.UBTTime) } @@ -677,6 +691,9 @@ func (c *ChainConfig) Description() string { if c.AmsterdamTime != nil { banner += fmt.Sprintf(" - Amsterdam: @%-10v\n", *c.AmsterdamTime) } + if c.BogotaTime != nil { + banner += fmt.Sprintf(" - Bogota: @%-10v\n", *c.BogotaTime) + } if c.UBTTime != nil { banner += fmt.Sprintf(" - UBT: @%-10v\n", *c.UBTTime) } @@ -854,6 +871,11 @@ func (c *ChainConfig) IsAmsterdam(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.AmsterdamTime, time) } +// IsBogota returns whether time is either equal to the Bogota fork time or greater. +func (c *ChainConfig) IsBogota(num *big.Int, time uint64) bool { + return c.IsLondon(num) && isTimestampForked(c.BogotaTime, time) +} + // IsUBT returns whether time is either equal to the Verkle fork time or greater. func (c *ChainConfig) IsUBT(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.UBTTime, time) @@ -940,6 +962,7 @@ func (c *ChainConfig) CheckConfigForkOrder() error { {name: "bpo4", timestamp: c.BPO4Time, optional: true}, {name: "bpo5", timestamp: c.BPO5Time, optional: true}, {name: "amsterdam", timestamp: c.AmsterdamTime, optional: true}, + {name: "bogota", timestamp: c.BogotaTime, optional: true}, } { if lastFork.name != "" { switch { @@ -1111,6 +1134,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, if isForkTimestampIncompatible(c.AmsterdamTime, newcfg.AmsterdamTime, headTimestamp) { return newTimestampCompatError("Amsterdam fork timestamp", c.AmsterdamTime, newcfg.AmsterdamTime) } + if isForkTimestampIncompatible(c.BogotaTime, newcfg.BogotaTime, headTimestamp) { + return newTimestampCompatError("Bogota fork timestamp", c.BogotaTime, newcfg.BogotaTime) + } return nil } @@ -1130,6 +1156,8 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { london := c.LondonBlock switch { + case c.IsBogota(london, time): + return forks.Bogota case c.IsAmsterdam(london, time): return forks.Amsterdam case c.IsBPO5(london, time): @@ -1213,6 +1241,10 @@ func (c *ChainConfig) ActiveSystemContracts(time uint64) map[string]common.Addre // the fork isn't defined or isn't a time-based fork. func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { switch { + case fork == forks.Bogota: + return c.BogotaTime + case fork == forks.Amsterdam: + return c.AmsterdamTime case fork == forks.BPO5: return c.BPO5Time case fork == forks.BPO4: @@ -1231,8 +1263,6 @@ func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { return c.CancunTime case fork == forks.Shanghai: return c.ShanghaiTime - case fork == forks.Amsterdam: - return c.AmsterdamTime default: return nil } @@ -1378,7 +1408,7 @@ type Rules struct { IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon bool IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool - IsAmsterdam, IsUBT bool + IsAmsterdam, IsBogota, IsUBT bool } // Rules ensures c's ChainID is not nil. @@ -1404,6 +1434,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules IsPrague: isMerge && c.IsPrague(num, timestamp), IsOsaka: isMerge && c.IsOsaka(num, timestamp), IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp), + IsBogota: isMerge && c.IsBogota(num, timestamp), IsUBT: isUBT, IsEIP4762: isUBT, } diff --git a/params/forks/forks.go b/params/forks/forks.go index 641d59434b..8308d15fbf 100644 --- a/params/forks/forks.go +++ b/params/forks/forks.go @@ -46,6 +46,7 @@ const ( BPO4 BPO5 Amsterdam + Bogota ) // String implements fmt.Stringer. @@ -84,4 +85,5 @@ var forkToString = map[Fork]string{ BPO4: "BPO4", BPO5: "BPO5", Amsterdam: "Amsterdam", + Bogota: "Bogota", }