Merge branch 'master' into bs/cell-blobpool/sparse-v2

This commit is contained in:
healthykim 2026-07-14 16:34:56 +02:00
commit 2f3a1628fe
78 changed files with 3521 additions and 941 deletions

View file

@ -10,7 +10,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
) )
var _ = (*executableDataMarshaling)(nil) var _ = (*executableDataMarshaling)(nil)
@ -18,25 +17,25 @@ var _ = (*executableDataMarshaling)(nil)
// MarshalJSON marshals as JSON. // MarshalJSON marshals as JSON.
func (e ExecutableData) MarshalJSON() ([]byte, error) { func (e ExecutableData) MarshalJSON() ([]byte, error) {
type ExecutableData struct { type ExecutableData struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"` ParentHash common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot common.Hash `json:"stateRoot" gencodec:"required"` StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random common.Hash `json:"prevRandao" gencodec:"required"` Random common.Hash `json:"prevRandao" gencodec:"required"`
Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash common.Hash `json:"blockHash" gencodec:"required"` BlockHash common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"` Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"`
} }
var enc ExecutableData var enc ExecutableData
enc.ParentHash = e.ParentHash enc.ParentHash = e.ParentHash
@ -69,25 +68,25 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
// UnmarshalJSON unmarshals from JSON. // UnmarshalJSON unmarshals from JSON.
func (e *ExecutableData) UnmarshalJSON(input []byte) error { func (e *ExecutableData) UnmarshalJSON(input []byte) error {
type ExecutableData struct { type ExecutableData struct {
ParentHash *common.Hash `json:"parentHash" gencodec:"required"` ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` StateRoot *common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random *common.Hash `json:"prevRandao" gencodec:"required"` Random *common.Hash `json:"prevRandao" gencodec:"required"`
Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash *common.Hash `json:"blockHash" gencodec:"required"` BlockHash *common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"` Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"`
} }
var dec ExecutableData var dec ExecutableData
if err := json.Unmarshal(input, &dec); err != nil { if err := json.Unmarshal(input, &dec); err != nil {
@ -165,7 +164,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
e.SlotNumber = (*uint64)(dec.SlotNumber) e.SlotNumber = (*uint64)(dec.SlotNumber)
} }
if dec.BlockAccessList != nil { if dec.BlockAccessList != nil {
e.BlockAccessList = dec.BlockAccessList e.BlockAccessList = *dec.BlockAccessList
} }
return nil return nil
} }

View file

@ -17,6 +17,7 @@
package engine package engine
import ( import (
"bytes"
"fmt" "fmt"
"math/big" "math/big"
"slices" "slices"
@ -25,7 +26,9 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal" "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/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
@ -83,40 +86,41 @@ type payloadAttributesMarshaling struct {
// ExecutableData is the data necessary to execute an EL payload. // ExecutableData is the data necessary to execute an EL payload.
type ExecutableData struct { type ExecutableData struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"` ParentHash common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot common.Hash `json:"stateRoot" gencodec:"required"` StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom []byte `json:"logsBloom" gencodec:"required"` LogsBloom []byte `json:"logsBloom" gencodec:"required"`
Random common.Hash `json:"prevRandao" gencodec:"required"` Random common.Hash `json:"prevRandao" gencodec:"required"`
Number uint64 `json:"blockNumber" gencodec:"required"` Number uint64 `json:"blockNumber" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"` GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"` GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Timestamp uint64 `json:"timestamp" gencodec:"required"` Timestamp uint64 `json:"timestamp" gencodec:"required"`
ExtraData []byte `json:"extraData" gencodec:"required"` ExtraData []byte `json:"extraData" gencodec:"required"`
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
BlockHash common.Hash `json:"blockHash" gencodec:"required"` BlockHash common.Hash `json:"blockHash" gencodec:"required"`
Transactions [][]byte `json:"transactions" gencodec:"required"` Transactions [][]byte `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"` Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *uint64 `json:"blobGasUsed"` BlobGasUsed *uint64 `json:"blobGasUsed"`
ExcessBlobGas *uint64 `json:"excessBlobGas"` ExcessBlobGas *uint64 `json:"excessBlobGas"`
SlotNumber *uint64 `json:"slotNumber,omitempty"` SlotNumber *uint64 `json:"slotNumber,omitempty"`
BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` BlockAccessList []byte `json:"blockAccessList,omitempty"`
} }
// JSON type overrides for executableData. // JSON type overrides for executableData.
type executableDataMarshaling struct { type executableDataMarshaling struct {
Number hexutil.Uint64 Number hexutil.Uint64
GasLimit hexutil.Uint64 GasLimit hexutil.Uint64
GasUsed hexutil.Uint64 GasUsed hexutil.Uint64
Timestamp hexutil.Uint64 Timestamp hexutil.Uint64
BaseFeePerGas *hexutil.Big BaseFeePerGas *hexutil.Big
ExtraData hexutil.Bytes ExtraData hexutil.Bytes
LogsBloom hexutil.Bytes LogsBloom hexutil.Bytes
Transactions []hexutil.Bytes Transactions []hexutil.Bytes
BlobGasUsed *hexutil.Uint64 BlobGasUsed *hexutil.Uint64
ExcessBlobGas *hexutil.Uint64 ExcessBlobGas *hexutil.Uint64
SlotNumber *hexutil.Uint64 SlotNumber *hexutil.Uint64
BlockAccessList hexutil.Bytes
} }
// StatelessPayloadStatusV1 is the result of a stateless payload execution. // StatelessPayloadStatusV1 is the result of a stateless payload execution.
@ -325,7 +329,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
// to be nil. // to be nil.
var blockAccessListHash *common.Hash var blockAccessListHash *common.Hash
if data.BlockAccessList != nil { if data.BlockAccessList != nil {
hash := data.BlockAccessList.Hash() hash := crypto.Keccak256Hash(data.BlockAccessList)
blockAccessListHash = &hash blockAccessListHash = &hash
} }
header := &types.Header{ header := &types.Header{
@ -352,32 +356,45 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
SlotNumber: data.SlotNumber, SlotNumber: data.SlotNumber,
BlockAccessListHash: blockAccessListHash, 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 // BlockToExecutableData constructs the ExecutableData structure by filling the
// fields from the given block. It assumes the given block is post-merge block. // 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 { func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope {
data := &ExecutableData{ data := &ExecutableData{
BlockHash: block.Hash(), BlockHash: block.Hash(),
ParentHash: block.ParentHash(), ParentHash: block.ParentHash(),
FeeRecipient: block.Coinbase(), FeeRecipient: block.Coinbase(),
StateRoot: block.Root(), StateRoot: block.Root(),
Number: block.NumberU64(), Number: block.NumberU64(),
GasLimit: block.GasLimit(), GasLimit: block.GasLimit(),
GasUsed: block.GasUsed(), GasUsed: block.GasUsed(),
BaseFeePerGas: block.BaseFee(), BaseFeePerGas: block.BaseFee(),
Timestamp: block.Time(), Timestamp: block.Time(),
ReceiptsRoot: block.ReceiptHash(), ReceiptsRoot: block.ReceiptHash(),
LogsBloom: block.Bloom().Bytes(), LogsBloom: block.Bloom().Bytes(),
Transactions: encodeTransactions(block.Transactions()), Transactions: encodeTransactions(block.Transactions()),
Random: block.MixDigest(), Random: block.MixDigest(),
ExtraData: block.Extra(), ExtraData: block.Extra(),
Withdrawals: block.Withdrawals(), Withdrawals: block.Withdrawals(),
BlobGasUsed: block.BlobGasUsed(), BlobGasUsed: block.BlobGasUsed(),
ExcessBlobGas: block.ExcessBlobGas(), ExcessBlobGas: block.ExcessBlobGas(),
SlotNumber: block.SlotNumber(), SlotNumber: block.SlotNumber(),
BlockAccessList: block.AccessList(), }
if al := block.AccessList(); al != nil {
var buf bytes.Buffer
if err := rlp.Encode(&buf, al); err == nil {
data.BlockAccessList = buf.Bytes()
}
} }
// Add blobs. // Add blobs.
@ -420,6 +437,12 @@ type ExecutionPayloadBody struct {
Withdrawals []*types.Withdrawal `json:"withdrawals"` 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. // Client identifiers to support ClientVersionV1.
const ( const (
ClientCode = "GE" ClientCode = "GE"

View file

@ -1,53 +1,53 @@
# This file contains sha256 checksums of optional build dependencies. # This file contains sha256 checksums of optional build dependencies.
# version:spec-tests v5.1.0 # version:spec-tests tests@v20.0.0
# https://github.com/ethereum/execution-spec-tests/releases # https://github.com/ethereum/execution-specs/releases
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 # https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz b183702a5b447b465873865357ced9eb342315922e52e31b714e2de115dd0bb4 fixtures.tar.gz
# version:golang 1.25.10 # version:golang 1.25.12
# https://go.dev/dl/ # https://go.dev/dl/
20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz f90dcee4bd023fa376374ea0a5a6ebe553537b39c426ffd8c689469b45519932 go1.25.12.src.tar.gz
a194e767c2ab4216a60acc068b9dbe6bf4fae05c14bb52d6bbdcb5b3ea521308 go1.25.10.aix-ppc64.tar.gz 70b4b6509ed60735eff6ed9496824ad9f96201fdf5bd190184123f5908f224ef go1.25.12.aix-ppc64.tar.gz
52321165a3146cd91865ef98371506a846ed4dc4f9f1c9323e5ad90d2a411e06 go1.25.10.darwin-amd64.tar.gz 00a2e743b82bccec03c51c4b0f7e46d5fec52184075fd6c5183c3bb39ae9fb00 go1.25.12.darwin-amd64.tar.gz
795691a425de7e7cdba3544f354dcd2cebcf52e87dc6898193878f34eb6d634f go1.25.10.darwin-arm64.tar.gz fa2c88bbcf64bd3b2aef355f026cfec6d3a4a01c132f999c8f8c964eb767164f go1.25.12.darwin-arm64.tar.gz
e37b4544ba9e9e9a7ab2ed3116b3fc4d39a88da854baa5a566d9d6d3a9de7d4c go1.25.10.dragonfly-amd64.tar.gz 2124cfbc1cf0b949eb819478365d4d665f84297a60f327cc65f75f61b2629b96 go1.25.12.dragonfly-amd64.tar.gz
2a70d1fdabab637aa442ca94599a56e381238efa20cb995d5433b8579bfe482c go1.25.10.freebsd-386.tar.gz bdb8ab506428e9633653e67c13d582a6230406f01037023b327427c66b058ff2 go1.25.12.freebsd-386.tar.gz
9cdf522d87d47d82fec4a313cc4f8c3c94a7770426e8d443e4150a1f330cba71 go1.25.10.freebsd-amd64.tar.gz 4433a424d466d47d1716df69e6a77c65b1a34d82121488014e4656d73740ebb0 go1.25.12.freebsd-amd64.tar.gz
6da6183633e9e59ffd9edefab68b5059c89b605596d94aaba650b1681fccd35f go1.25.10.freebsd-arm.tar.gz 429fbcb46468a66d1cd24129a1b6163167676aaae8e0989a08ba95ba6aae65c4 go1.25.12.freebsd-arm.tar.gz
7adcefeebdd05331f4d45f1ad2dddb5c53537cff6552e82f6595b3b833b95371 go1.25.10.freebsd-arm64.tar.gz c0e31a4cb827fd20fac950bcb4688fd94cdd1491dbdcff7c3754ac8f3b136eb1 go1.25.12.freebsd-arm64.tar.gz
285f80a1ace21a7d94035cd753196eeada8cacd48e6396fd116ad5eb67aea957 go1.25.10.freebsd-riscv64.tar.gz 0ea50c6d43e809d46c2c5504e97ef11822f7ca137625171924cda971449b5373 go1.25.12.freebsd-riscv64.tar.gz
de7461bf0e5068a4f6e7f8713026d70516be6dbd5de5d21f9ced1c182f2f326e go1.25.10.illumos-amd64.tar.gz d74c214e0c3ae8f9db23f4ec6f6b2f6cb300c3e4b9d840b82fa517af629c455d go1.25.12.illumos-amd64.tar.gz
2f574f2e2e19ead5b280fec0e7af5c81b76632685f03b6ac42dfa34c4b773c52 go1.25.10.linux-386.tar.gz b71e88ae779850dc2afcd0f2e2208798652311dfda72fdef5d05721d601b8fd3 go1.25.12.linux-386.tar.gz
42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70 go1.25.10.linux-amd64.tar.gz 234828b7a89e0e303d2556310ee549fbcf253d28de937bac3da13d6294262ac1 go1.25.12.linux-amd64.tar.gz
654da1f9b50a5d1c2a85ccf8ed405aa89c06e94d18384628bf186f7712677b08 go1.25.10.linux-arm64.tar.gz 8b5884aef89600aef5b0b051fb971f11f49bb996521e911f30f02a66884f7bd2 go1.25.12.linux-arm64.tar.gz
39f168f158e693887d3ad006168af1b1a3007b19c5993cae4d9d57f82f52aaf8 go1.25.10.linux-armv6l.tar.gz 6cd7311c02c73ba0b482a1cf8c885268edf23519261bf4b5cef3353ad934d1f1 go1.25.12.linux-armv6l.tar.gz
05401fe5ea50ad2bafb9c797ef9bf21574b0661f19ef4d0dd66af8a0fb7323f3 go1.25.10.linux-loong64.tar.gz 0c6b2b7db8509d1df189433709ddc8fe84c12843e3713508e9ca04dc9e75ddeb go1.25.12.linux-loong64.tar.gz
d5bc2d6155d394a3aae41f21eb7c60da5595a6147aa0f30ed6b27da25e06c3f7 go1.25.10.linux-mips.tar.gz 5abc63471425ab8b3408f596cd547d28d374f6dc860a4cbd6de79497123db4f3 go1.25.12.linux-mips.tar.gz
8c64e7493e5953c3ba3153487d2fddd7f8ed142392c77f138e6792a6c1930db4 go1.25.10.linux-mips64.tar.gz a18f7a5ac799ed8ac264b63707e7fa475b1b81da6bbdd41c394fcfd69cc6b736 go1.25.12.linux-mips64.tar.gz
bd53aa2d558b7c1eadfc6bf01132e1859203a92f458ed7ba75b7f3230f14b095 go1.25.10.linux-mips64le.tar.gz 5bde8a5d4c05b428fca3e6022dc41d4c735858be9c6945e57473da00fcddd0cc go1.25.12.linux-mips64le.tar.gz
120b254e2e2980bb06687175db5c4064a85696c53001dc9f59934ad18f74a6bc go1.25.10.linux-mipsle.tar.gz 33c243d2b0700589e75a410ebab5b64580a477e908872abc46eef840e6ea535a go1.25.12.linux-mipsle.tar.gz
8a6acb21295b0ec974a44608361920ea8dbff5666631a6f556bd7d5f1d56535f go1.25.10.linux-ppc64.tar.gz b5288a7adb38540177146b47aa43bc75f1936c5e14026d6b6e531b534a49eb1a go1.25.12.linux-ppc64.tar.gz
778925fdcdf9a272f823d147fad51545c3334b7ccd8652b2ccaaf2b01800280a go1.25.10.linux-ppc64le.tar.gz 64adb4ddefef4f0a6f11af550547f39bf510350da69ab308438a21eacfde97ad go1.25.12.linux-ppc64le.tar.gz
b4f04ad0db48bcfea946db5323919cd21034e0bd2821a557dacd29c1b1013a4b go1.25.10.linux-riscv64.tar.gz 26919d62d21b0bee9c5c67ad76ace462edd16c2c128a983d942cb45b0bf7693c go1.25.12.linux-riscv64.tar.gz
936b953e43921a64c12da871f76871ebbeb6d2092a7b8bdc307f5246f3c662cc go1.25.10.linux-s390x.tar.gz 09875de1d6cb3237437112271b9df3a96bac9fcd10cbf9bc777c00e67f4e3e3d go1.25.12.linux-s390x.tar.gz
061470e0bc7132146a5925a3cc28d5bc498eb1b1ff09dedcfaae10f781ff2274 go1.25.10.netbsd-386.tar.gz 2f19f4afc3d6551804228484569ec1ebf4a57a91cda75d746384aa71d5016be7 go1.25.12.netbsd-386.tar.gz
63b2d50d7f8f269a9c82d42a4060e90cffb7f9102299818bb071b067aac8da8f go1.25.10.netbsd-amd64.tar.gz d2237237d34058658187455d4f05f8f4f5f2118b33827308ad8313ab0f00a693 go1.25.12.netbsd-amd64.tar.gz
c35129f68796526aa4dc4b6f481e2d995ef312aedadc88b659b945cc00e1f8f0 go1.25.10.netbsd-arm.tar.gz 4d44c8141a829541c3499db6665a47be2c6fdb97903575058809a2a8be53af89 go1.25.12.netbsd-arm.tar.gz
2f541da4e2b298154d992d1f11bbb38c89d0821d91cc50a46776d42bb5e63bca go1.25.10.netbsd-arm64.tar.gz fc96791f8b9cdaea544e827dc15574731afd28b7378053e801b3bd466df6dd9d go1.25.12.netbsd-arm64.tar.gz
2d42e569b07f1b99fdbfd008e7c22f967d165e2ce02464f46818fbed2aec43f5 go1.25.10.openbsd-386.tar.gz e9ced39c191409207a211c6013b9a156bd90dbd5a76ebd8c66dc3bf69bcb2f9e go1.25.12.openbsd-386.tar.gz
0ad05960e8c9f867328151308c87f938433bec8f22f6a9437a896e22169fc840 go1.25.10.openbsd-amd64.tar.gz bae7c016f0aff806c9af08ecff96b59a232144c96b1d25f4c5f6c85c8e0dbf01 go1.25.12.openbsd-amd64.tar.gz
099cc11473f99461c77161912740945308f08f6834980afb262c72bdc915f2d7 go1.25.10.openbsd-arm.tar.gz 8517c4bca20e975acdd5a2f5e425cd2bec737bbb6d7ee18d11e3a1545014267e go1.25.12.openbsd-arm.tar.gz
bdf3335d5008c1ddc81fa94892283e4f1fee22566f5351d4e726d9f55a67c838 go1.25.10.openbsd-arm64.tar.gz ba4ac29243f43b85a52f12ed7dd02b767e4524785fb6d04fececb7ce125aaa82 go1.25.12.openbsd-arm64.tar.gz
0933d418da0a61e0f29de717a77498f16b9b5b50dbe2205e20b2ed7fd4067f75 go1.25.10.openbsd-ppc64.tar.gz dca067f41d00ba805342062ab21b88831a06a5682e9aaec1c990c175a5656c6b go1.25.12.openbsd-ppc64.tar.gz
191e6f3e75712f8c13d189d53b668e2cac6449f26474c1d86fbd04f6e9846f9c go1.25.10.openbsd-riscv64.tar.gz 4f1f5376464fc91f32cd253dadfe3535ff57d1729b24e6ab014fb83964dfe10f go1.25.12.openbsd-riscv64.tar.gz
68c053c8acd76c50fc430e92f4a86110ec3d97dd03d27b9339b4eaf793caff5f go1.25.10.plan9-386.tar.gz c938792ec65ba33592deea6673265d509aaf9b5297119bc91ae973fb6beb62b9 go1.25.12.plan9-386.tar.gz
42e2c46638ae22d93402e79efb40faee5c42cf7c56a01bb3ab47c6bb2512b745 go1.25.10.plan9-amd64.tar.gz 5aec7ab115c1c6cd049a64f1617ef91d24ea07c8b0f40436d531b42d546f5e37 go1.25.12.plan9-amd64.tar.gz
3ef1d5838b1648da16724a07b72e839ccbd7cb8899c3e0426afd6b79d494b91c go1.25.10.plan9-arm.tar.gz efe1dcb9750507260a28688a749c72f35c3160646f08a80606f38c3d30d74a12 go1.25.12.plan9-arm.tar.gz
631e3716017fbec06500a628d97e1155daec3593f0a7812c2ebfe8fc8c96b2ab go1.25.10.solaris-amd64.tar.gz 83c42cfedcc0bb03fd601d692d75b085406c9a7f5ff505bf41030f2734b62ed2 go1.25.12.solaris-amd64.tar.gz
ddc693d2d9d7cc671ebb72d1d50aa05670f95b059b7d90440611af57976871d5 go1.25.10.windows-386.zip 18abdf76719f5f84eaa35eeaf87b467a02ed075a8632ad941f10aa7a3d0de713 go1.25.12.windows-386.zip
ca37af2dadd8544464f1a9ca7c3886499d1cdfcb263855d0a1d71f194b2bd222 go1.25.10.windows-amd64.zip d5dc82da351b00e5eedd04f41356817d674cc4308131f0f638a5b14c5c3af4cb go1.25.12.windows-amd64.zip
38be57e0398bd93673d65bcae6dc7ee3cf151d7038d0dba5c60a5153022872da go1.25.10.windows-arm64.zip 054f046a5fa31fdcc9491cc19065cbf43bf521d805bbe298ae8d65dd981fca84 go1.25.12.windows-arm64.zip
# version:golangci 2.10.1 # version:golangci 2.10.1
# https://github.com/golangci/golangci-lint/releases/ # https://github.com/golangci/golangci-lint/releases/

View file

@ -452,7 +452,7 @@ func doTest(cmdline []string) {
// downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures. // downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures.
func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string {
ext := ".tar.gz" ext := ".tar.gz"
base := "fixtures_develop" base := "fixtures"
archivePath := filepath.Join(cachedir, base+ext) archivePath := filepath.Join(cachedir, base+ext)
if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil {
log.Fatal(err) log.Fatal(err)

View file

@ -140,15 +140,15 @@ func Transaction(ctx *cli.Context) error {
value = uint256.NewInt(1) value = uint256.NewInt(1)
} }
rules := chainConfig.Rules(common.Big0, true, 0) 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 { if err != nil {
r.Error = err r.Error = err
results = append(results, r) results = append(results, r)
continue continue
} }
r.IntrinsicGas = cost.RegularGas r.IntrinsicGas = cost
if tx.Gas() < cost.RegularGas { if tx.Gas() < cost {
r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), cost.RegularGas) r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), cost)
results = append(results, r) results = append(results, r)
continue continue
} }

View file

@ -175,6 +175,7 @@ var (
utils.RPCGlobalEVMTimeoutFlag, utils.RPCGlobalEVMTimeoutFlag,
utils.RPCGlobalTxFeeCapFlag, utils.RPCGlobalTxFeeCapFlag,
utils.RPCGlobalLogQueryLimit, utils.RPCGlobalLogQueryLimit,
utils.EngineMaxReorgDepthFlag,
utils.AllowUnprotectedTxs, utils.AllowUnprotectedTxs,
utils.BatchRequestLimit, utils.BatchRequestLimit,
utils.BatchResponseMaxSize, utils.BatchResponseMaxSize,

View file

@ -17,7 +17,7 @@ require (
github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/emicklei/dot v1.6.2 // 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/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect
github.com/ferranbt/fastssz v0.1.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect

View file

@ -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/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 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 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.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58=
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/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 h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= 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= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=

View file

@ -686,6 +686,12 @@ var (
Value: ethconfig.Defaults.RangeLimit, Value: ethconfig.Defaults.RangeLimit,
Category: flags.APICategory, 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 // Authenticated RPC HTTP settings
AuthListenFlag = &cli.StringFlag{ AuthListenFlag = &cli.StringFlag{
Name: "authrpc.addr", Name: "authrpc.addr",
@ -1925,6 +1931,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(RPCGlobalTxFeeCapFlag.Name) { if ctx.IsSet(RPCGlobalTxFeeCapFlag.Name) {
cfg.RPCTxFeeCap = ctx.Float64(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) { if ctx.Bool(NoDiscoverFlag.Name) {
cfg.EthDiscoveryURLs, cfg.SnapDiscoveryURLs = []string{}, []string{} cfg.EthDiscoveryURLs, cfg.SnapDiscoveryURLs = []string{}, []string{}
} else if ctx.IsSet(DNSDiscoveryFlag.Name) { } else if ctx.IsSet(DNSDiscoveryFlag.Name) {

View file

@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
data := make([]byte, nbytes) data := make([]byte, nbytes)
return func(i int, gen *BlockGen) { return func(i int, gen *BlockGen) {
toaddr := common.Address{} 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() signer := gen.Signer()
gasPrice := big.NewInt(0) gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil { if gen.header.BaseFee != nil {
@ -99,7 +99,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
Nonce: gen.TxNonce(benchRootAddr), Nonce: gen.TxNonce(benchRootAddr),
To: &toaddr, To: &toaddr,
Value: big.NewInt(1), Value: big.NewInt(1),
Gas: cost.RegularGas, Gas: cost,
Data: data, Data: data,
GasPrice: gasPrice, GasPrice: gasPrice,
}) })

View file

@ -65,12 +65,12 @@ var (
func TestProcessUBT(t *testing.T) { func TestProcessUBT(t *testing.T) {
var ( var (
code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) 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 // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness
// will not contain that copied data. // will not contain that copied data.
// Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985
codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) 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) signer = types.LatestSigner(testUBTChainConfig)
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain
@ -102,11 +102,11 @@ func TestProcessUBT(t *testing.T) {
txCost1 := params.TxGas txCost1 := params.TxGas
txCost2 := params.TxGas txCost2 := params.TxGas
contractCreationCost := intrinsicContractCreationGas.RegularGas + contractCreationCost := intrinsicContractCreationGas +
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* creation with value */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* creation with value */
739 /* execution costs */ 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 (tx) */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (CREATE at pc=0x20) */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (CREATE at pc=0x20) */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* write code hash */ params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* write code hash */

View file

@ -913,7 +913,7 @@ func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*typ
// noState represents if the target state requested for search // noState represents if the target state requested for search
// is unavailable and impossible to be recovered. // 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 start = time.Now() // Timestamp the rewinding is restarted
logged = time.Now() // Timestamp last progress log was printed 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 // Check if the associated state is available or recoverable if
// the requested root has already been crossed. // 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 break
} }
// If pivot block is reached, return the genesis block as the // 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 return head, rootNumber
} }
} }
// Recover if the target state if it's not available yet. // Note, the state of the located head may not be physically present yet if
if !bc.HasState(head.Root) { // it's only recoverable. The actual recovery is intentionally deferred once
if err := bc.triedb.Recover(head.Root); err != nil { // the new head is finalized, so that a deep rewind rolls the state back in
log.Error("Failed to rollback state, resetting to genesis", "err", err) // one shot.
return bc.genesisBlock.Header(), rootNumber log.Info("Rewound to block with available state", "number", head.Number, "hash", head.Hash())
}
}
log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash())
return head, rootNumber return head, rootNumber
} }
@ -1034,17 +1031,9 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
bc.currentBlock.Store(newHeadBlock) bc.currentBlock.Store(newHeadBlock)
headBlockGauge.Update(int64(newHeadBlock.Number.Uint64())) headBlockGauge.Update(int64(newHeadBlock.Number.Uint64()))
// The head state is missing, which is only possible in the path-based // Note, the located head state might not be physically present yet; in
// scheme. This situation occurs when the chain head is rewound below // the path-based scheme a recoverable state is materialized in a single
// the pivot point. In this scenario, there is no possible recovery // shot once the rewind is finalized.
// 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())
}
} }
// Rewind the snap block in a simpleton way to the target head // Rewind the snap block in a simpleton way to the target head
if currentSnapBlock := bc.CurrentSnapBlock(); currentSnapBlock != nil && header.Number.Uint64() < currentSnapBlock.Number.Uint64() { 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) 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 // Clear out any stale content from the caches
bc.bodyCache.Purge() bc.bodyCache.Purge()
bc.bodyRLPCache.Purge() bc.bodyRLPCache.Purge()
@ -2402,7 +2416,7 @@ func (bc *BlockChain) insertSideChain(ctx context.Context, block *types.Block, i
) )
parent := it.previous() parent := it.previous()
for parent != nil && !bc.HasState(parent.Root) { 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 { if err := bc.triedb.Recover(parent.Root); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -2464,7 +2478,7 @@ func (bc *BlockChain) recoverAncestors(ctx context.Context, block *types.Block,
parent = block parent = block
) )
for parent != nil && !bc.HasState(parent.Root()) { 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 { if err := bc.triedb.Recover(parent.Root()); err != nil {
return common.Hash{}, err return common.Hash{}, err
} }

View file

@ -395,11 +395,11 @@ func (bc *BlockChain) HasBlockAndState(hash common.Hash, number uint64) bool {
return bc.HasState(block.Root()) 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 // Note, this function assumes the state is not present, because
// state is not treated as recoverable if it's available, thus // state is not treated as recoverable if it's available, thus
// false will be returned in this case. // 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 { if bc.triedb.Scheme() == rawdb.HashScheme {
return false return false
} }

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -37,59 +38,67 @@ func TestEIP2780Intrinsic(t *testing.T) {
name string name string
to *common.Address to *common.Address
value *uint256.Int value *uint256.Int
want vm.GasCosts auths []types.SetCodeAuthorization
want uint64
}{ }{
{ {
name: "self-transfer", name: "self-transfer",
to: &from, to: &from,
value: uint256.NewInt(1), value: uint256.NewInt(1),
want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 want: params.TxBaseCost2780, // 12,000
}, },
{ {
name: "self-transfer/zero-value", name: "self-transfer/zero-value",
to: &from, to: &from,
value: uint256.NewInt(0), value: uint256.NewInt(0),
want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 want: params.TxBaseCost2780, // 12,000
}, },
{ {
name: "zero-value call", name: "zero-value call",
to: &to, to: &to,
value: uint256.NewInt(0), value: uint256.NewInt(0),
// TxBaseCost + ColdAccountAccess = 15,000 // TxBaseCost + ColdAccountAccess = 15,000; the recipient touch is
want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780}, // charged at the cold rate unconditionally at the intrinsic phase.
want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam,
}, },
{ {
name: "value transfer to existing EOA", name: "value transfer to existing EOA",
to: &to, to: &to,
value: uint256.NewInt(1), value: uint256.NewInt(1),
// TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000 // TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000
want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
params.TxValueCost2780 + params.TransferLogCost2780}, params.TxValueCost2780 + params.TransferLogCost2780,
}, },
{ {
name: "contract creation, value = 0", name: "contract creation, value = 0",
to: nil, to: nil,
value: uint256.NewInt(0), value: uint256.NewInt(0),
// TxBaseCost + CreateAccess = 23,000 regular, plus one account creation in state. // TxBaseCost + CreateAccess = 23,000 regular. The new-account state
want: vm.GasCosts{ // charge depends on whether the deployment target exists and is
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, // charged at runtime, not intrinsically.
StateGas: params.AccountCreationSize * params.CostPerStateByte, want: params.TxBaseCost2780 + params.CreateAccessAmsterdam,
},
}, },
{ {
name: "contract creation, value > 0", name: "contract creation, value > 0",
to: nil, to: nil,
value: uint256.NewInt(1), value: uint256.NewInt(1),
// TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular, plus account creation. // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular.
want: vm.GasCosts{ want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780,
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, },
StateGas: params.AccountCreationSize * params.CostPerStateByte, {
}, 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 { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { 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 { if err != nil {
t.Fatalf("unexpected error: %v", err) 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. // (intrinsic + top-level + execution) recorded in the block gas pool.
func TestEIP2780Gas(t *testing.T) { func TestEIP2780Gas(t *testing.T) {
const ( const (
cold = params.ColdAccountAccess2780 cold = params.ColdAccountAccessAmsterdam
base = params.TxBaseCost2780 base = params.TxBaseCost2780
valueCst = params.TxValueCost2780 + params.TransferLogCost2780 valueCst = params.TxValueCost2780 + params.TransferLogCost2780
) )
@ -154,9 +163,9 @@ func TestEIP2780Gas(t *testing.T) {
// case 8: ETH transfer creating a new account. // case 8: ETH transfer creating a new account.
{"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState}, {"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState},
// case 9: contract-creation transaction, value = 0. // 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. // 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 { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { 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 // callTxAL builds a signed dynamic-fee call carrying an access list.
// account both materializes and funds the recipient. func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types.AccessList) *types.Transaction {
func TestEIP2780NewAccountFunded(t *testing.T) { return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
fresh := common.HexToAddress("0xbeef000000000000000000000000000000000002") ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value),
sdb := mkState(senderAlloc(nil)) Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al,
if _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 300_000, nil)); err != nil { })
}
// 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) t.Fatal(err)
} }
if !sdb.Exist(fresh) || sdb.GetBalance(fresh).Cmp(uint256.NewInt(1)) != 0 { if res.Err != nil {
t.Fatalf("recipient not funded: exist=%v balance=%v", sdb.Exist(fresh), sdb.GetBalance(fresh)) 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) { if sdb.Exist(fresh) {
t.Fatal("recipient should not be created when the call charge cannot be paid") 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)
}
})
} }

View file

@ -21,6 +21,7 @@
package core package core
import ( import (
"errors"
"math/big" "math/big"
"testing" "testing"
@ -72,6 +73,40 @@ func mkState(alloc types.GenesisAlloc) *state.StateDB {
return sdb 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. // amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM { func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
ctx := vm.BlockContext{ 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) t.Fatalf("to message: %v", err)
} }
gp := NewGasPool(evm.Context.GasLimit) 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)) evm.SetTxContext(NewEVMTxContext(msg))
st := newStateTransition(evm, msg, gp) st := newStateTransition(evm, msg, gp)
res, err := st.execute() res, err := st.execute()
if err == nil && res != nil { if err == nil && res != nil {
assertPoolSane(t, res, gp) floor, ferr := FloorDataGas(rules8037, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList)
limit := min(msg.GasLimit, params.MaxTxGas) if ferr != nil {
assertBudgetSane(t, vm.NewGasBudget(limit, msg.GasLimit-limit), st.gasRemaining) 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 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. // assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
// //
// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas // 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 // 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() t.Helper()
if gp.cumulativeUsed != res.UsedGas { if gp.cumulativeUsed != res.UsedGas {
t.Fatalf("receipt scalar = %d, want UsedGas %d", 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 { if res.UsedGas > res.MaxUsedGas {
t.Fatalf("post-refund gas %d exceeds peak %d", 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 { if gp.cumulativeState > res.MaxUsedGas {
t.Fatalf("regular+state %d exceeds peak %d", sum, 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) { if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
t.Fatalf("block used %d != max(%d,%d)", gp.Used(), 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 ( var (
deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
haltI = []byte{0xfe, 0x00, 0x00, 0x00, 0x00} // init: INVALID, exceptional halt
) )
// ===================== Top-level create transaction ====================== // ===================== Top-level create transaction ======================
// A creation tx's intrinsic gas pre-charges one account creation as state gas. // A creation tx's intrinsic gas is state-independent: the new-account state
func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) { // charge depends on whether the deployment target exists and is charged at
cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte) // runtime (EIP-2780), not intrinsically.
func TestCreateTxIntrinsicNoStateGas(t *testing.T) {
cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cost.StateGas != newAccountState { if want := params.TxBaseCost2780 + params.CreateAccessAmsterdam; cost != want {
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState) t.Fatalf("intrinsic gas = %d, want %d", cost, want)
} }
} }
// Creating onto a pre-existing (balance-only) address refills the account // Creating onto a pre-existing (balance-only) address incurs no new-account
// portion; only the code deposit is charged as state gas. // runtime charge; only the code deposit is charged as state gas.
func TestCreateTxPreexistingDestRefill(t *testing.T) { func TestCreateTxPreexistingDestRefill(t *testing.T) {
derived := crypto.CreateAddress(senderAddr, 0) derived := crypto.CreateAddress(senderAddr, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}})) 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) { func TestCreateTxRevertRefill(t *testing.T) {
sdb := mkState(senderAlloc(nil)) sdb := mkState(senderAlloc(nil))
res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI)) 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) { func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
const gas = 1_000_000 const gas = 1_000_000
derived := crypto.CreateAddress(senderAddr, 0) derived := crypto.CreateAddress(senderAddr, 0)
@ -241,14 +296,185 @@ func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
if !res.Failed() { if !res.Failed() {
t.Fatal("expected collision failure") 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 { if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState) t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
} }
// All forwarded gas_left is burned; only the refilled account charge (which if res.UsedGas > gas-newAccountState {
// had spilled into regular) returns to gas_left. So regular gas consumed is t.Fatalf("used gas = %d, want at most %d (charge not refilled?)", res.UsedGas, gas-newAccountState)
// 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 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 { for i := range al {
al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1))) al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1)))
} }
tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ tx := types.MustSignNewTx(senderKey, signer8037,
ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0), &types.DynamicFeeTx{
Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al, 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 { if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil {
t.Fatal("expected rejection for intrinsic regular over MaxTxGas") 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 =========================== // ========================= Refund and gas used ===========================
// clearSlots deploys a contract that zeroes slots 1..n, each preset to 1. // 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") var delegate8037 = common.HexToAddress("0xde1e8a7e")
// Intrinsic gas pre-charges the worst-case (account + indicator) per auth. // Intrinsic gas charges only the state-independent per-authorization base;
func TestAuthIntrinsicWorstCase(t *testing.T) { // the state-dependent charges are applied at runtime (EIP-2780).
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte) func TestAuthIntrinsicBaseOnly(t *testing.T) {
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cost.StateGas != authWorstState { // The recipient touch and the per-authorization authority access (priced
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState) // 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) { func TestAuthInvalidRefillFull(t *testing.T) {
k, _ := crypto.HexToECDSA(authKeyA) k, _ := crypto.HexToECDSA(authKeyA)
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ 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) { func TestAuthAccountExistsRefill(t *testing.T) {
auth, authority := signAuth(t, authKeyA, delegate8037, 0) auth, authority := signAuth(t, authKeyA, delegate8037, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})) sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))
@ -508,12 +775,12 @@ func TestAuthAccountExistsRefill(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if gp.cumulativeState != authBaseState { 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 // Setting a delegation on an already-delegated authority writes no net-new
// portion (and the account portion, since the authority already exists). // bytes (and no account leaf, since the authority exists): no state charge.
func TestAuthSetOnDelegatedRefillBase(t *testing.T) { func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
auth, authority := signAuth(t, authKeyA, delegate8037, 0) auth, authority := signAuth(t, authKeyA, delegate8037, 0)
pre := types.AddressToDelegation(common.HexToAddress("0xabcd")) pre := types.AddressToDelegation(common.HexToAddress("0xabcd"))
@ -523,11 +790,12 @@ func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if gp.cumulativeState != 0 { 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) { func TestAuthSetNetNewNoRefill(t *testing.T) {
auth, _ := signAuth(t, authKeyA, delegate8037, 0) auth, _ := signAuth(t, authKeyA, delegate8037, 0)
sdb := mkState(senderAlloc(nil)) sdb := mkState(senderAlloc(nil))
@ -536,11 +804,12 @@ func TestAuthSetNetNewNoRefill(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if gp.cumulativeState != authWorstState { 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) { func TestAuthClearRefillBase(t *testing.T) {
auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO) auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO)
sdb := mkState(senderAlloc(nil)) sdb := mkState(senderAlloc(nil))
@ -549,13 +818,14 @@ func TestAuthClearRefillBase(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if want := newAccountState; gp.cumulativeState != want { 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 // 0->a->0 in one tx: the indicator charge applies when the delegation is set
// later one writes zero net bytes, so both indicator charges refill. // and is never credited back when a later auth clears it in the same
func TestAuthClearSameTxDoubleRefill(t *testing.T) { // transaction.
func TestAuthClearSameTxNoRefill(t *testing.T) {
set, authority := signAuth(t, authKeyA, delegate8037, 0) set, authority := signAuth(t, authKeyA, delegate8037, 0)
clr, _ := signAuth(t, authKeyA, common.Address{}, 1) clr, _ := signAuth(t, authKeyA, common.Address{}, 1)
sdb := mkState(senderAlloc(nil)) sdb := mkState(senderAlloc(nil))
@ -564,8 +834,28 @@ func TestAuthClearSameTxDoubleRefill(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
_ = authority _ = authority
if want := newAccountState; gp.cumulativeState != want { if want := authWorstState; gp.cumulativeState != want {
t.Fatalf("state gas = %d, want %d (net-zero delegation)", 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)
} }
} }

View file

@ -14,14 +14,6 @@
// You should have received a copy of the GNU Lesser General Public License // You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// 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 package core
import ( import (
@ -37,74 +29,117 @@ import (
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
// newAuthTestTransition builds a minimal stateTransition with a state reservoir, // newAuthTestTransition builds a minimal stateTransition with a runtime gas
// suitable for calling applyAuthorization directly. // budget, suitable for calling applyAuthorization directly.
func newAuthTestTransition(sdb *state.StateDB) *stateTransition { func newAuthTestTransition(sdb *state.StateDB) *stateTransition {
st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000)) 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 return st
} }
// A net-new delegation on a fresh authority writes a new account leaf, so the // A net-new delegation on a fresh, cold authority is charged ACCOUNT_WRITE in
// intrinsic ACCOUNT_WRITE stands (no refund). // regular gas (the authority's cold access is paid unconditionally at the
func TestAuthAccountWriteNetNewNoRefund(t *testing.T) { // 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) auth, _ := signAuth(t, authKeyA, delegate8037, 0)
st := newAuthTestTransition(mkState(senderAlloc(nil))) 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) t.Fatal(err)
} }
if got := st.state.GetRefund(); got != 0 { if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
t.Fatalf("refund = %d, want 0 (net-new account write)", got) 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 // A pre-existing authority writes no new account leaf, but its first write in
// ACCOUNT_WRITE is refunded. // the transaction still carries ACCOUNT_WRITE; the authority's cold access is
func TestAuthAccountWriteExistsRefund(t *testing.T) { // 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) auth, authority := signAuth(t, authKeyA, delegate8037, 0)
st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) 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) t.Fatal(err)
} }
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam) 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 // No cold surcharge is ever charged at runtime — the authority access is priced
// intrinsic ACCOUNT_WRITE is refunded. // at the intrinsic phase — so an authority already warmed by the access list or
func TestAuthAccountWriteInvalidRefund(t *testing.T) { // 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) k, _ := crypto.HexToECDSA(authKeyA)
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
}) })
st := newAuthTestTransition(mkState(senderAlloc(nil))) 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") t.Fatal("expected invalid-authorization error")
} }
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { if st.gasRemaining.UsedRegularGas != 0 || st.gasRemaining.UsedStateGas != 0 {
t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam) 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 // The same authority across two authorizations is charged once: the first auth
// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the // warms the authority, materializes the account and installs the indicator, so
// account as existing) is refunded. // the second incurs no further charge.
func TestAuthAccountWriteDuplicateOnce(t *testing.T) { func TestAuthRuntimeDuplicateAuthorityOnce(t *testing.T) {
a0, _ := signAuth(t, authKeyA, delegate8037, 0) a0, _ := signAuth(t, authKeyA, delegate8037, 0)
a1, _ := signAuth(t, authKeyA, delegate8037, 1) a1, _ := signAuth(t, authKeyA, delegate8037, 1)
st := newAuthTestTransition(mkState(senderAlloc(nil))) st := newAuthTestTransition(mkState(senderAlloc(nil)))
delegates := map[common.Address]bool{} authorities := map[common.Address]*authTracking{}
if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil { if err := st.applyAuthorization(rules8037, &a0, authorities); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got := st.state.GetRefund(); got != 0 { if err := st.applyAuthorization(rules8037, &a1, authorities); err != nil {
t.Fatalf("refund after first auth = %d, want 0", got)
}
if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam) 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")
} }
} }

View file

@ -137,4 +137,9 @@ var (
ErrAuthorizationInvalidSignature = errors.New("EIP-7702 authorization has invalid signature") ErrAuthorizationInvalidSignature = errors.New("EIP-7702 authorization has invalid signature")
ErrAuthorizationDestinationHasCode = errors.New("EIP-7702 authorization destination is a contract") ErrAuthorizationDestinationHasCode = errors.New("EIP-7702 authorization destination is a contract")
ErrAuthorizationNonceMismatch = errors.New("EIP-7702 authorization nonce does not match current account nonce") 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")
) )

View file

@ -176,8 +176,8 @@ func WriteFinalizedBlockHash(db ethdb.KeyValueWriter, hash common.Hash) {
// ReadLastPivotNumber retrieves the number of the last pivot block. If the node // 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 // 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 // is written during snap sync and never cleared, so that a rewind below the
// pivot can re-enable snap sync. // pivot can be detected.
func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 { func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 {
data, _ := db.Get(lastPivotKey) data, _ := db.Get(lastPivotKey)
if len(data) == 0 { if len(data) == 0 {

View file

@ -70,7 +70,7 @@ func InitDatabaseFromFreezer(db ethdb.Database) {
i += uint64(len(data)) i += uint64(len(data))
// If we've spent too much time already, notify the user of what we're doing // If we've spent too much time already, notify the user of what we're doing
if time.Since(logged) > 8*time.Second { 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() logged = time.Now()
} }
} }

View file

@ -46,7 +46,7 @@ var (
// persistentStateIDKey tracks the id of latest stored state(for path-based only). // persistentStateIDKey tracks the id of latest stored state(for path-based only).
persistentStateIDKey = []byte("LastStateID") 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") lastPivotKey = []byte("LastPivot")
// fastTrieProgressKey tracks the number of trie entries imported during fast sync. // fastTrieProgressKey tracks the number of trie entries imported during fast sync.

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "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/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "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. // 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 isContractCreation := to == nil
// Set the starting gas for the raw transaction // Set the starting gas for the raw transaction
var gas vm.GasCosts var gas uint64
if rules.IsAmsterdam { if rules.IsAmsterdam {
gas.RegularGas = intrinsicBaseGasEIP2780(from, to, value) gas = intrinsicBaseGasEIP2780(from, to, value)
if isContractCreation {
// New-account creation is charged as state gas (EIP-8037).
gas.StateGas = params.AccountCreationSize * costPerStateByte
}
} else if isContractCreation && rules.IsHomestead { } else if isContractCreation && rules.IsHomestead {
gas.RegularGas = params.TxGasContractCreation gas = params.TxGasContractCreation
} else { } else {
gas.RegularGas = params.TxGas gas = params.TxGas
} }
// Add gas for authorizations // Add gas for authorizations
if authList != nil { if authList != nil {
if rules.IsAmsterdam { if rules.IsAmsterdam {
gas.RegularGas += uint64(len(authList)) * (params.AccountWriteAmsterdam + params.RegularPerAuthBaseCost) gas += uint64(len(authList)) * params.RegularPerAuthBaseCost
gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte
} else { } 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 // Bump the required gas by the amount of transactional data
dataLen := uint64(len(data))
if dataLen > 0 { if dataLen > 0 {
// Zero and non-zero bytes are priced differently // Zero and non-zero bytes are priced differently
z := uint64(bytes.Count(data, []byte{0})) z := uint64(bytes.Count(data, []byte{0}))
@ -105,24 +101,25 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
if rules.IsIstanbul { if rules.IsIstanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028 nonZeroGas = params.TxDataNonZeroGasEIP2028
} }
if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz { if (math.MaxUint64-gas)/nonZeroGas < nz {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += nz * nonZeroGas gas += nz * nonZeroGas
if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z { if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += z * params.TxDataZeroGas gas += z * params.TxDataZeroGas
if isContractCreation && rules.IsShanghai { if isContractCreation && rules.IsShanghai {
lenWords := toWordSize(dataLen) lenWords := toWordSize(dataLen)
if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords { if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += lenWords * params.InitCodeWordGas gas += lenWords * params.InitCodeWordGas
} }
} }
// Add the gas for accessList
if accessList != nil { if accessList != nil {
addresses := uint64(len(accessList)) addresses := uint64(len(accessList))
storageKeys := uint64(accessList.StorageKeys()) storageKeys := uint64(accessList.StorageKeys())
@ -134,14 +131,14 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
addressCost = params.TxAccessListAddressGasAmsterdam addressCost = params.TxAccessListAddressGasAmsterdam
storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam
} }
if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { if (math.MaxUint64-gas)/addressCost < addresses {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += addresses * addressCost gas += addresses * addressCost
if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { if (math.MaxUint64-gas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += storageKeys * storageKeyCost gas += storageKeys * storageKeyCost
// EIP-7981: access list data is charged in addition to the base charge. // EIP-7981: access list data is charged in addition to the base charge.
if rules.IsAmsterdam { if rules.IsAmsterdam {
@ -149,38 +146,41 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
) )
if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { if (math.MaxUint64-gas)/addressCost < addresses {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += addresses * addressCost gas += addresses * addressCost
if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { if (math.MaxUint64-gas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
gas.RegularGas += storageKeys * storageKeyCost gas += storageKeys * storageKeyCost
} }
} }
return gas, nil return gas, nil
} }
// intrinsicBaseGasEIP2780 computes the regular-gas portion of the EIP-2780 // intrinsicBaseGasEIP2780 computes the intrinsic base cost of the transaction.
// intrinsic base cost: the per-resource decomposition of the legacy flat 21,000.
func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int) uint64 { func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int) uint64 {
var ( var (
isContractCreation = to == nil isContractCreation = to == nil
isSelfTransfer = to != nil && *to == from isSelfTransfer = to != nil && *to == from
hasValue = value != nil && !value.IsZero() 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 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 { switch {
case isSelfTransfer: case isSelfTransfer:
// The recipient account is already accessed and written as the sender. // The recipient account is already accessed and written as the sender.
case isContractCreation: case isContractCreation:
gas += params.CreateAccess2780 gas += params.CreateAccessAmsterdam
default: default:
gas += params.ColdAccountAccess2780 gas += params.ColdAccountAccessAmsterdam
} }
// tx.value charge. // tx.value charge.
@ -242,10 +242,12 @@ func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, v
tokenCost = params.TxCostFloorPerToken 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 floorBase := params.TxGas
if rules.IsAmsterdam { if rules.IsAmsterdam {
floorBase = params.TxBaseCost2780 floorBase = intrinsicBaseGasEIP2780(from, to, value)
} }
// Check for overflow // Check for overflow
if (math.MaxUint64-floorBase)/tokenCost < tokens { if (math.MaxUint64-floorBase)/tokenCost < tokens {
@ -260,7 +262,6 @@ func toWordSize(size uint64) uint64 {
if size > math.MaxUint64-31 { if size > math.MaxUint64-31 {
return math.MaxUint64/32 + 1 return math.MaxUint64/32 + 1
} }
return (size + 31) / 32 return (size + 31) / 32
} }
@ -423,24 +424,16 @@ func (st *stateTransition) to() common.Address {
return *st.msg.To return *st.msg.To
} }
// buyGas pre-pays gas from the sender's balance and initializes the // buyGas pre-pays gas from the sender's balance.
// transaction's gas budget. It is invoked at the tail of preCheck.
// //
// The balance requirement is the worst-case ETH the tx may need to lock // The balance requirement is the worst-case ETH the tx may need to lock
// up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`, // up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`,
// plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance // plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance
// returns ErrInsufficientFunds. After the check, the sender is actually // returns ErrInsufficientFunds.
// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee`
// under Cancun), the cap-vs-tip differential is settled at tx end.
// //
// The gas budget is seeded into both `initialBudget` (frozen snapshot // After the check, the sender is actually debited `msg.GasLimit × msg.GasPrice`
// for tx-end accounting) and `gasRemaining` (live running balance): // (plus `blobGas × blobBaseFee` under Cancun), the cap-vs-tip differential
// // is settled at tx end.
// - 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.
func (st *stateTransition) buyGas() error { func (st *stateTransition) buyGas() error {
mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
_, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) _, 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 { 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) 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 // Deduct the gas cost from the sender's balance
st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy)
return nil 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 // 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: // It returns a consensus error if any of the following fail:
// //
// - Sender nonce matches state and is not at 2^64-1 (EIP-2681). // - 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 // - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam.
// it is enforced there via the two-dimensional budget in buyGas). //
// - EIP-3607 sender-is-EOA, allowing accounts whose only code is an // - EIP-3607 sender-is-EOA, allowing accounts whose only code is an
// EIP-7702 delegation designator. // EIP-7702 delegation designator.
//
// - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+). // - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+).
//
// - Blob-tx structural checks: non-nil `To`, non-empty hash list, // - Blob-tx structural checks: non-nil `To`, non-empty hash list,
// valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+). // valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+).
//
// - Blob fee-cap not below the current blob base fee (Cancun+). // - Blob fee-cap not below the current blob base fee (Cancun+).
//
// - EIP-7702 set-code-tx shape: non-nil `To` and non-empty // - EIP-7702 set-code-tx shape: non-nil `To` and non-empty
// authorization list. // 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 // The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass
// subsets of these checks for simulation paths (eth_call, eth_estimateGas). // 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 // Only check transactions that are not fake
msg := st.msg msg := st.msg
if !msg.SkipNonceChecks { if !msg.SkipNonceChecks {
@ -557,13 +559,9 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce) 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 { if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap. // 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) return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
} }
// Make sure the sender is an EOA // 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) // 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) // 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 skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0
if !skipCheck { if !skipCheck {
@ -601,7 +599,7 @@ func (st *stateTransition) preCheck() error {
if len(msg.BlobHashes) == 0 { if len(msg.BlobHashes) == 0 {
return ErrMissingBlobHashes return ErrMissingBlobHashes
} }
if isOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { if rules.IsOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs {
return ErrTooManyBlobs return ErrTooManyBlobs
} }
for i, hash := range msg.BlobHashes { 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 // 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 { if st.blobGasUsed() > 0 {
// Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call) // Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call)
skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() == 0 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) 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() return st.buyGas()
} }
@ -649,32 +663,25 @@ func (st *stateTransition) preCheck() error {
// If a consensus error is encountered, it is returned directly with a // If a consensus error is encountered, it is returned directly with a
// nil EVM execution result. // nil EVM execution result.
func (st *stateTransition) execute() (*ExecutionResult, error) { 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 ( var (
msg = st.msg msg = st.msg
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
contractCreation = msg.To == nil contractCreation = msg.To == nil
floorDataGas uint64 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 { if err != nil {
return nil, err return nil, err
} }
prior, sufficient := st.gasRemaining.Charge(cost) if msg.GasLimit < intrinsicGas {
if !sufficient { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, intrinsicGas)
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas)
} }
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 // 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. // the total gas usage at tx end, so the gas limit must be sufficient to cover that.
if rules.IsPrague { if rules.IsPrague {
@ -687,13 +694,15 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if msg.GasLimit < floorDataGas { if msg.GasLimit < floorDataGas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, 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. // In Amsterdam, the transaction gas limit is allowed to exceed
if rules.IsAmsterdam && max(cost.RegularGas, floorDataGas) > params.MaxTxGas { // params.MaxTxGas, but the intrinsic cost and calldata floor
return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas) // 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 { if rules.IsEIP4762 {
st.evm.AccessEvents.AddTxOrigin(msg.From) st.evm.AccessEvents.AddTxOrigin(msg.From)
@ -718,56 +727,18 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// - enable block-level accessList construction (EIP-7928) // - enable block-level accessList construction (EIP-7928)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) 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 // Execute the top-most frame
var ( var (
ret []byte ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err vmerr error // vm errors do not effect consensus
result vm.GasBudget
) )
if contractCreation { if contractCreation {
// Check whether the init code size has been exceeded. ret, vmerr = st.executeCreate(rules, value)
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)
}
} else { } else {
// Increment the nonce for the next transaction. ret, vmerr = st.executeCall(rules, value)
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)
}
} }
// Settle down the gas usage and refund the ETH back if any remaining // Settle down the gas usage and refund the ETH back if any remaining
@ -808,43 +779,150 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}, nil }, nil
} }
// chargeCallRecipientEIP2780 applies the EIP-2780 transaction top-level gas costs for // executeCreate runs the top-level frame of a contract-creation transaction
// a message-call transaction, charged before any opcode executes: // and returns the EVM return data and the frame-level execution error.
// func (st *stateTransition) executeCreate(rules params.Rules, value *uint256.Int) ([]byte, error) {
// - if the recipient is EIP-161 non-existent and the transaction carries value, msg := st.msg
// charge for account creation.
// var chargedCreation bool
// - if the recipient is an EIP-7702 delegated account, resolving the delegation if rules.IsAmsterdam {
// loads the target's code, charged an additional cold account access in addr := crypto.CreateAddress(msg.From, st.state.GetNonce(msg.From))
// regular gas. if st.state.Empty(addr) {
func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) {
var ( // The nonce increment normally performed inside evm.Create
cost vm.GasCosts // must still happen for the included transaction.
to = *st.msg.To st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeContractCreator)
) st.gasRemaining = st.gasRemaining.ExitHalt()
// This runs in the topmost frame before any bytecode executes, so unlike the return nil, vm.ErrOutOfGas
// 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 chargedCreation = true
// !Exist is equivalent to Empty. }
if !value.IsZero() && !st.state.Exist(to) {
cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte
} }
if _, ok := types.ParseDelegation(st.state.GetCode(to)); ok { // The first frame is entered with the gas remaining after the runtime
// EIP-2780: The tx.sender, tx.to, and (where applicable) delegation-target // charges.
// charges above are always at the cold rate. ret, _, result, vmerr := st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
// st.gasRemaining.Absorb(result)
// The delegation-target is already warmed before, no double warming here.
cost.RegularGas += params.ColdAccountAccess2780 // 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{}) { // If the top-most frame halted, drain the leftover regular gas rather
return true // 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) prior, ok := st.gasRemaining.Charge(cost)
if !ok { if !ok {
return false return false
} }
if st.evm.Config.Tracer.HasGasHook() { 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 return true
} }
@ -852,27 +930,11 @@ func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool {
// settleGas finalizes the per-tx gas accounting after EVM execution: // settleGas finalizes the per-tx gas accounting after EVM execution:
// //
// - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas, // - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas,
// tx_state_gas) before any refund or floor: // tx_state_gas) before any refund.
//
// 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
//
// - Computes the receipt scalar tx_gas_used by applying the EIP-3529 // - 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 // refund and the EIP-7623 calldata floor.
// calldata floor:
//
// tx_gas_used = max(tx_gas_used_before_refund - tx_gas_refund, calldata_floor)
//
// - Charges the block gas pool (2D under Amsterdam, scalar pre-Amsterdam). // - Charges the block gas pool (2D under Amsterdam, scalar pre-Amsterdam).
//
// - Refunds the leftover gas to the sender as ETH. // - 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) { func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (gasUsed, peakUsed uint64, err error) {
if st.gasRemaining.UsedStateGas < 0 { if st.gasRemaining.UsedStateGas < 0 {
return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas) return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas)
@ -881,15 +943,15 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
// EIP-8037: // EIP-8037:
// tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir // 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 = 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 gasLeft := st.gasRemaining.RegularGas + st.gasRemaining.StateGas
gasUsedBeforeRefund := st.msg.GasLimit - gasLeft gasUsedBeforeRefund := st.msg.GasLimit - gasLeft
if gasUsedBeforeRefund < txStateGas { if gasUsedBeforeRefund < txStateGas {
return 0, 0, fmt.Errorf("negative topmost frame regular gas usage, total: %d, state: %d", 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). // EIP-3529: tx_gas_refund = min(tx_gas_used_before_refund/5, refund_counter).
refund := st.calcRefund(gasUsedBeforeRefund) refund := st.calcRefund(gasUsedBeforeRefund)
@ -911,6 +973,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g
peakUsed = max(peakUsed, floorDataGas) peakUsed = max(peakUsed, floorDataGas)
} }
// Settle down the final gas consumption in the block-level pool
if rules.IsAmsterdam { if rules.IsAmsterdam {
if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil {
return 0, 0, err 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 { if gasLeft > 0 {
refund := new(uint256.Int).Mul(uint256.NewInt(gasLeft), st.msg.GasPrice) refund := new(uint256.Int).Mul(uint256.NewInt(gasLeft), st.msg.GasPrice)
st.state.AddBalance(st.msg.From, refund, tracing.BalanceIncreaseGasReturn) st.state.AddBalance(st.msg.From, refund, tracing.BalanceIncreaseGasReturn)
@ -964,51 +1027,71 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio
return authority, nil return authority, nil
} }
// applyAuthorization applies an EIP-7702 code delegation to the state and, // authTracking tracks the charges already paid for an authority by earlier
// adjust the pre-charged intrinsic cost accordingly. // authorizations in the same transaction.
func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { 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) authority, err := st.validateAuthorization(auth)
if err != nil { if err != nil {
if rules.IsAmsterdam {
st.gasRemaining.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte)
st.state.AddRefund(params.AccountWriteAmsterdam)
}
return err return err
} }
prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) oldDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority))
if !rules.IsAmsterdam { if !rules.IsAmsterdam {
if st.state.Exist(authority) { if st.state.Exist(authority) {
st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
} }
} else { } else {
if st.state.Exist(authority) { // EIP-2780: charge the state-dependent authorization costs at runtime.
st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) // The authority's cold access was already charged unconditionally at the
st.state.AddRefund(params.AccountWriteAmsterdam) // intrinsic phase, so only state-dependent costs remain here.
} var cost vm.GasCosts
authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte
preDelegated, ok := delegates[authority] track := authorities[authority]
if !ok { if track == nil {
preDelegated = curDelegated track = &authTracking{authBaseCovered: curDelegated}
delegates[authority] = preDelegated authorities[authority] = track
} }
if auth.Address == (common.Address{}) { // Every valid authorization writes the authority account: the
// Clearing writes no indicator, refill this auth's state charge. // nonce bump, and possibly the delegation indicator. The first
st.gasRemaining.RefundStateToReservoir(authBase) // write to an account within the transaction carries the
// first-write surcharge. At this point the accounts whose write
// The indicator was created by an earlier auth within the same // has already been paid for are:
// transaction, refill the state charge as it's no longer justified. //
if curDelegated && !preDelegated { // - the sender: TX_BASE_COST prices its account write, and the
st.gasRemaining.RefundStateToReservoir(authBase) // gas prepayment and nonce bump have already happened;
} //
} else if curDelegated || preDelegated { // - authorities written by preceding valid authorizations in
// The 23-byte slot is already occupied, overwriting it writes no // this list, which carried the surcharge themselves;
// new bytes, refill the state charge. //
st.gasRemaining.RefundStateToReservoir(authBase) // - 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. // Update nonce and account code.
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) 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 return nil
} }
// Install delegation to auth.Address if the delegation changed // 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) st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
} }
return nil return nil
} }
// applyAuthorizations applies an EIP-7702 code delegation to the state. // applyAuthorizations applies the EIP-7702 code delegations to the state.
func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { // It reports whether the transaction budget covered all runtime authorization
preDelegated := make(map[common.Address]bool) // charges.
func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) bool {
authorities := make(map[common.Address]*authTracking)
for _, auth := range auths { 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. // calcRefund computes the EIP-3529 refund cap against tx_gas_used_before_refund.

View file

@ -22,7 +22,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -158,50 +157,50 @@ func TestIntrinsicGas(t *testing.T) {
isEIP3860 bool isEIP3860 bool
isAmsterdam bool isAmsterdam bool
value *uint256.Int value *uint256.Int
want vm.GasCosts want uint64
}{ }{
{ {
name: "frontier/empty-call", name: "frontier/empty-call",
want: vm.GasCosts{RegularGas: params.TxGas}, want: params.TxGas,
}, },
{ {
name: "frontier/contract-creation-pre-homestead", name: "frontier/contract-creation-pre-homestead",
creation: true, creation: true,
isHomestead: false, isHomestead: false,
// pre-homestead, contract creation still uses TxGas // pre-homestead, contract creation still uses TxGas
want: vm.GasCosts{RegularGas: params.TxGas}, want: params.TxGas,
}, },
{ {
name: "homestead/contract-creation", name: "homestead/contract-creation",
creation: true, creation: true,
isHomestead: true, isHomestead: true,
want: vm.GasCosts{RegularGas: params.TxGasContractCreation}, want: params.TxGasContractCreation,
}, },
{ {
name: "frontier/non-zero-data", name: "frontier/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100), data: bytes.Repeat([]byte{0xff}, 100),
// 100 nz bytes * 68 (frontier) // 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", name: "istanbul/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100), data: bytes.Repeat([]byte{0xff}, 100),
isEIP2028: true, isEIP2028: true,
// 100 nz bytes * 16 (post-EIP2028) // 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", name: "istanbul/zero-data",
data: bytes.Repeat([]byte{0x00}, 100), data: bytes.Repeat([]byte{0x00}, 100),
isEIP2028: true, isEIP2028: true,
// 100 zero bytes * 4 // 100 zero bytes * 4
want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas}, want: params.TxGas + 100*params.TxDataZeroGas,
}, },
{ {
name: "istanbul/mixed-data", name: "istanbul/mixed-data",
data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
isEIP2028: true, 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", name: "shanghai/init-code-word-gas",
@ -211,7 +210,7 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isEIP3860: true, isEIP3860: true,
// TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 // 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", name: "shanghai/init-code-non-multiple-of-32",
@ -220,7 +219,7 @@ func TestIntrinsicGas(t *testing.T) {
isHomestead: true, isHomestead: true,
isEIP2028: true, isEIP2028: true,
isEIP3860: 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", name: "berlin/access-list",
@ -230,7 +229,7 @@ func TestIntrinsicGas(t *testing.T) {
}, },
isEIP2028: true, isEIP2028: true,
// 2 addrs * 2400 + 3 keys * 1900 // 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", name: "amsterdam/access-list-extra-cost",
@ -241,10 +240,12 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
// EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess // EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess
// (15,000). Plus base access-list charge + EIP-7981 extra. // (15,000); the recipient touch is charged at the cold rate
want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + // 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*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam +
2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost,
}, },
{ {
name: "prague/auth-list", name: "prague/auth-list",
@ -255,7 +256,7 @@ func TestIntrinsicGas(t *testing.T) {
}, },
isEIP2028: true, isEIP2028: true,
// 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple) // 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", name: "amsterdam/contract-creation-empty",
@ -263,12 +264,9 @@ func TestIntrinsicGas(t *testing.T) {
isHomestead: true, isHomestead: true,
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
// EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000), // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000);
// and account-creation cost is charged as state gas. // the new-account state charge is applied at runtime.
want: vm.GasCosts{ want: params.TxBaseCost2780 + params.CreateAccessAmsterdam,
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780,
StateGas: params.AccountCreationSize * params.CostPerStateByte,
},
}, },
{ {
name: "amsterdam/contract-creation-init-code", name: "amsterdam/contract-creation-init-code",
@ -278,11 +276,8 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isEIP3860: true, // Shanghai gates init-code word gas isEIP3860: true, // Shanghai gates init-code word gas
isAmsterdam: true, isAmsterdam: true,
want: vm.GasCosts{ want: params.TxBaseCost2780 + params.CreateAccessAmsterdam +
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
StateGas: params.AccountCreationSize * params.CostPerStateByte,
},
}, },
{ {
name: "amsterdam/contract-creation-with-access-list", name: "amsterdam/contract-creation-with-access-list",
@ -295,13 +290,10 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isEIP3860: true, isEIP3860: true,
isAmsterdam: true, isAmsterdam: true,
want: vm.GasCosts{ want: params.TxBaseCost2780 + params.CreateAccessAmsterdam +
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas +
32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam +
1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost,
1*amsterdamAddressCost + 1*amsterdamStorageKeyCost,
StateGas: params.AccountCreationSize * params.CostPerStateByte,
},
}, },
{ {
name: "amsterdam/combined", name: "amsterdam/combined",
@ -314,18 +306,15 @@ func TestIntrinsicGas(t *testing.T) {
}, },
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
// EIP-8037 splits the auth-tuple charge into regular + state gas, with // EIP-2780: the recipient touch and the per-authorization authority
// the values finalized by EIP-8038: // access (priced into RegularPerAuthBaseCost) are both charged at the
// regular: ACCOUNT_WRITE (8,000) + REGULAR_PER_AUTH_BASE_COST (7,500) per auth // cold rate unconditionally at the intrinsic phase; the account leaf
// state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth // and indicator bytes are charged at runtime.
want: vm.GasCosts{ want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + 100*params.TxDataNonZeroGasEIP2028 +
100*params.TxDataNonZeroGasEIP2028 + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam +
1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + 1*params.RegularPerAuthBaseCost,
1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost),
StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte,
},
}, },
{ {
name: "amsterdam/value-transfer-call", name: "amsterdam/value-transfer-call",
@ -333,8 +322,8 @@ func TestIntrinsicGas(t *testing.T) {
isAmsterdam: true, isAmsterdam: true,
value: uint256.NewInt(1), value: uint256.NewInt(1),
// EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000. // EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000.
want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + want: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
params.TransferLogCost2780 + params.TxValueCost2780}, params.TransferLogCost2780 + params.TxValueCost2780,
}, },
{ {
name: "amsterdam/value-bearing-contract-creation", name: "amsterdam/value-bearing-contract-creation",
@ -343,11 +332,9 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
value: uint256.NewInt(1), value: uint256.NewInt(1),
// EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756, plus account-creation state gas. // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756;
want: vm.GasCosts{ // the new-account state charge is applied at runtime.
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, want: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780,
StateGas: params.AccountCreationSize * params.CostPerStateByte,
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -363,7 +350,7 @@ func TestIntrinsicGas(t *testing.T) {
to = &addr1 to = &addr1
} }
got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, 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 { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }

View file

@ -29,21 +29,23 @@ func _() {
_ = x[GasChangeWitnessContractCollisionCheck-18] _ = x[GasChangeWitnessContractCollisionCheck-18]
_ = x[GasChangeTxDataFloor-19] _ = x[GasChangeTxDataFloor-19]
_ = x[GasChangeRefundAccountCreation-20] _ = x[GasChangeRefundAccountCreation-20]
_ = x[GasChangeTxRuntimeGas-21]
_ = x[GasChangeAccountCreation-22]
_ = x[GasChangeIgnored-255] _ = x[GasChangeIgnored-255]
} }
const ( const (
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreation" _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreationTxRuntimeGasAccountCreation"
_GasChangeReason_name_1 = "Ignored" _GasChangeReason_name_1 = "Ignored"
) )
var ( 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 { func (i GasChangeReason) String() string {
switch { switch {
case i <= 20: case i <= 22:
return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]]
case i == 255: case i == 255:
return _GasChangeReason_name_1 return _GasChangeReason_name_1

View file

@ -476,6 +476,15 @@ const (
// pre-charged account-creation cost when no account is created. // pre-charged account-creation cost when no account is created.
GasChangeRefundAccountCreation GasChangeReason = 20 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 // 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. // it will be "manually" tracked by a direct emit of the gas change event.
GasChangeIgnored GasChangeReason = 0xFF GasChangeIgnored GasChangeReason = 0xFF

View file

@ -129,12 +129,12 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
} }
// Ensure the transaction has more gas than the bare minimum needed to cover // Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata // 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 { if err != nil {
return err return err
} }
if tx.Gas() < intrGas.RegularGas { if tx.Gas() < intrGas {
return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas.RegularGas) return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas)
} }
// Ensure the transaction can cover floor data gas. // Ensure the transaction can cover floor data gas.
if rules.IsPrague { if rules.IsPrague {
@ -149,8 +149,8 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
} }
// In Amsterdam, the transaction gas limit is allowed to exceed // In Amsterdam, the transaction gas limit is allowed to exceed
// params.MaxTxGas, but the calldata floor cost is capped by it. // params.MaxTxGas, but the calldata floor cost is capped by it.
if rules.IsAmsterdam && max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas { if rules.IsAmsterdam && max(intrGas, floorDataGas) > params.MaxTxGas {
return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas) 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 // Ensure the gasprice is high enough to cover the requirement of the calling pool

View file

@ -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 fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, size, params.MaxInitCodeSize)
} }
} }
return nil return nil
} }

View file

@ -215,6 +215,8 @@ func activePrecompiledContracts(rules params.Rules) PrecompiledContracts {
switch { switch {
case rules.IsUBT: case rules.IsUBT:
return PrecompiledContractsVerkle return PrecompiledContractsVerkle
case rules.IsBogota:
return PrecompiledContractsOsaka
case rules.IsOsaka: case rules.IsOsaka:
return PrecompiledContractsOsaka return PrecompiledContractsOsaka
case rules.IsPrague: case rules.IsPrague:
@ -240,6 +242,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts {
// ActivePrecompiles returns the precompile addresses enabled with the current configuration. // ActivePrecompiles returns the precompile addresses enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address { func ActivePrecompiles(rules params.Rules) []common.Address {
switch { switch {
case rules.IsBogota:
return PrecompiledAddressesOsaka
case rules.IsOsaka: case rules.IsOsaka:
return PrecompiledAddressesOsaka return PrecompiledAddressesOsaka
case rules.IsPrague: case rules.IsPrague:

View file

@ -150,6 +150,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
evm.precompiles = activePrecompiledContracts(evm.chainRules) evm.precompiles = activePrecompiledContracts(evm.chainRules)
switch { switch {
case evm.chainRules.IsBogota:
evm.table = &bogotaInstructionSet
case evm.chainRules.IsAmsterdam: case evm.chainRules.IsAmsterdam:
evm.table = &amsterdamInstructionSet evm.table = &amsterdamInstructionSet
case evm.chainRules.IsOsaka: case evm.chainRules.IsOsaka:
@ -473,20 +475,57 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
return ret, exitGas, err return ret, exitGas, err
} }
// create creates a new contract using code as deployment code. // createFramePreCheck the precondition before executing the contract deployment,
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) { // halts the create frame if fails with any check below.
// Depth check execution. Fail if we're trying to execute above the func (evm *EVM) createFramePreCheck(caller common.Address, value *uint256.Int) error {
// limit.
var nonce uint64
if evm.depth > int(params.CallCreateDepth) { if evm.depth > int(params.CallCreateDepth) {
err = ErrDepth return ErrDepth
} else if !evm.Context.CanTransfer(evm.StateDB, caller, value) { }
err = ErrInsufficientBalance if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
} else { return ErrInsufficientBalance
nonce = evm.StateDB.GetNonce(caller) }
if nonce+1 < nonce { nonce := evm.StateDB.GetNonce(caller)
err = ErrNonceUintOverflow 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 { if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig())
@ -495,17 +534,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
}(gas) }(gas)
} }
if err != nil { 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 // 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 // Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 { if evm.chainRules.IsEIP4762 {
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
if !ok { if !ok {
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
} }
if evm.Config.Tracer.HasGasHook() { if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
@ -532,7 +571,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
} }
// EIP-8037 collision rule: the state reservoir is fully preserved on // EIP-8037 collision rule: the state reservoir is fully preserved on
// address collision while regular gas is burnt. // 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. // 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 // It might be possible the contract code is deployed to a pre-existent
@ -540,7 +579,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
snapshot := evm.StateDB.Snapshot() snapshot := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) { if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(address) evm.StateDB.CreateAccount(address)
creation = true
} }
// CreateContract means that regardless of whether the account previously existed // CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account. // in the state trie or not, it _now_ becomes created as a _contract_ account.
@ -555,7 +593,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.chainRules.IsEIP4762 { if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted { if consumed < wanted {
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas
} }
prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() { if evm.Config.Tracer.HasGasHook() {
@ -586,11 +624,11 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) 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). // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved).
// Both packaged as a success-form GasBudget. // 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 // initNewContract runs a new contract's creation code, performs checks on the
@ -646,7 +684,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
} }
// Create creates a new contract using code as deployment code. // 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)) contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE) return evm.create(caller, code, gas, value, contractAddr, CREATE)
} }
@ -655,7 +693,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:] // 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. // 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) inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)

View file

@ -560,12 +560,10 @@ func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
words := (size + 31) / 32 words := (size + 31) / 32
wordGas := params.InitCodeWordGas * words wordGas := params.InitCodeWordGas * words
// Unconditionally pre-charge the account creation and refunds if the creation // The account-creation state gas is not part of the opcode cost: it is
// doesn't happen after the create-frame. // charged conditionally at the destination access, in the creating frame,
return GasCosts{ // right before the 63/64ths split (see opCreate).
RegularGas: gas + wordGas, return GasCosts{RegularGas: gas + wordGas}, nil
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
}, nil
} }
func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { 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). // (for address hashing).
wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words
// Unconditionally pre-charge the account creation and refunds if the creation // The account-creation state gas is not part of the opcode cost: it is
// doesn't happen after the create-frame. // charged conditionally at the destination access, in the creating frame,
return GasCosts{ // right before the 63/64ths split (see opCreate2).
RegularGas: gas + wordGas, return GasCosts{RegularGas: gas + wordGas}, nil
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
}, nil
} }
// regularGasCall8038 is the intrinsic regular-gas calculator for CALL in // 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() transfersValue = !stack.back(2).IsZero()
address = common.Address(stack.back(1).Bytes20()) address = common.Address(stack.back(1).Bytes20())
) )
// TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated? // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist
// It's technically possible to skip the EIP158 but very unlikely in practice. // in the current state yet still be considered non-existent by EIP-161 if its
if evm.chainRules.IsEIP158 { // nonce, balance, and code are all zero. Such accounts can appear temporarily
// Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist // during execution (e.g. via SELFDESTRUCT) and are removed at tx end.
// 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 // Funding such an account makes it permanent state growth and must be charged.
// during execution (e.g. via SELFDESTRUCT) and are removed at tx end. if transfersValue && evm.StateDB.Empty(address) {
//
// 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) {
gas += params.AccountCreationSize * evm.Context.CostPerStateByte gas += params.AccountCreationSize * evm.Context.CostPerStateByte
} }
return gas, nil return gas, nil
@ -698,22 +688,26 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor
var ( var (
y, x = stack.back(1), stack.peek() y, x = stack.back(1), stack.peek()
slot = common.Hash(x.Bytes32()) slot = common.Hash(x.Bytes32())
value = common.Hash(y.Bytes32())
stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte
) )
// Check slot presence in the access list // Check slot presence in the access list
access := params.WarmStorageReadCostEIP2929 access := params.WarmStorageAccessAmsterdam
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot)
if !slotPresent {
access = params.ColdStorageAccessAmsterdam access = params.ColdStorageAccessAmsterdam
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
} }
// Check access cost affordability before reading slot // Check access cost affordability before reading slot
if contract.Gas.RegularGas < access { if contract.Gas.RegularGas < access {
return GasCosts{}, errors.New("not enough gas for slot 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 // 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) if current == value { // noop (1)
return GasCosts{RegularGas: access}, nil return GasCosts{RegularGas: access}, nil
} }

View file

@ -48,18 +48,6 @@ func (g GasCosts) String() string {
// - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross // - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross
// consumption. UsedStateGas is signed so it can be decremented by inline // consumption. UsedStateGas is signed so it can be decremented by inline
// state-gas refunds (e.g., SSTORE 0->A->0). // 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 { type GasBudget struct {
RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb)
StateGas uint64 // remaining state-gas reservoir (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} return GasBudget{RegularGas: regular, StateGas: state}
} }
// Used returns the total scalar gas consumed relative to an initial budget // 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).
func (g GasBudget) Used(initial GasBudget) uint64 { func (g GasBudget) Used(initial GasBudget) uint64 {
return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) 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 // Charge deducts a combined regular+state cost from the running balance and
// updates the usage accumulators. State-gas in excess of the reservoir spills // updates the usage accumulators.
// into regular_gas.
func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
prior := *g prior := *g
ok := g.charge(cost) ok := g.charge(cost)
return prior, ok return prior, ok
} }
// chargeRegularOnly deducts a regular-only cost. // ChargeRegularOnly deducts a regular-only cost. It's always preferred for
func (g *GasBudget) chargeRegularOnly(r uint64) bool { // performance consideration if the opcode doesn't have any state cost.
func (g *GasBudget) ChargeRegularOnly(r uint64) bool {
if g.RegularGas < r { if g.RegularGas < r {
return false 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 // 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, // without going out of gas.
// and any state gas in excess of the reservoir must be coverable by the
// remaining regular gas (the spillover), mirroring charge without mutating.
func (g GasBudget) CanAfford(cost GasCosts) bool { func (g GasBudget) CanAfford(cost GasCosts) bool {
if g.RegularGas < cost.RegularGas { if g.RegularGas < cost.RegularGas {
return false return false
@ -162,8 +146,7 @@ func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) {
return g.Charge(GasCosts{RegularGas: r}) return g.Charge(GasCosts{RegularGas: r})
} }
// ChargeState is a convenience that deducts a state-only cost (spills to // ChargeState is a convenience that deducts a state-only cost.
// regular when the reservoir is exhausted). Returns false on OOG.
func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) { func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) {
return g.Charge(GasCosts{StateGas: s}) 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). // 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) { func (g *GasBudget) RefundState(s uint64) {
repay := min(s, g.Spilled) repay := min(s, g.Spilled)
g.RegularGas += repay g.RegularGas += repay
g.Spilled -= repay g.Spilled -= repay
// Whatever is left tops up the reservoir.
g.StateGas += s - repay g.StateGas += s - repay
g.UsedStateGas -= int64(s) g.UsedStateGas -= int64(s)
} }
// RefundStateToReservoir credits a state-gas refund directly to the // DrainRegular burns the remaining regular-gas.
// reservoir, without repaying spilled regular gas first. func (g *GasBudget) DrainRegular() {
// g.UsedRegularGas += g.RegularGas
// Per the spec's set_delegation, authorization refunds (and the post-create g.RegularGas = 0
// 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)
} }
// Forward drains `regular` regular gas and the entire state reservoir from // Forward drains `regular` regular gas and the entire state reservoir from
// the parent's running budget and returns the initial GasBudget for a child // 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 // frame. The parent's UsedRegularGas is bumped by the forwarded amount so
// that the absorb-on-return path correctly reclaims the unused portion. // 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 { func (g *GasBudget) Forward(regular uint64) GasBudget {
g.RegularGas -= regular g.RegularGas -= regular
g.UsedRegularGas += regular g.UsedRegularGas += regular
@ -249,19 +200,15 @@ func (g *GasBudget) ForwardAll() GasBudget {
// absorb to update its own state. // absorb to update its own state.
// ============================================================================ // ============================================================================
// ExitSuccess produces the leftover form for a successful frame. Inline // ExitSuccess produces the leftover form for a successful frame.
// state-gas refunds have already been folded into StateGas / UsedStateGas
// during execution; the running budget IS the exit budget on success.
func (g GasBudget) ExitSuccess() GasBudget { func (g GasBudget) ExitSuccess() GasBudget {
return g return g
} }
// ExitRevert produces the leftover for a REVERT exit. The frame's state // 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 // changes are discarded, so all state gas it charged is refilled with LIFO
// (EIP-8037): up to Spilled is returned to RegularGas (the regular // mechanism: up to Spilled is returned to RegularGas (the regular gas it
// gas it borrowed), and the remainder restores the reservoir. Because the // borrowed), and the remainder restores the reservoir.
// borrowed regular gas is repaid first, the reservoir is made whole back to its
// start-of-frame value.
func (g GasBudget) ExitRevert() GasBudget { func (g GasBudget) ExitRevert() GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled) reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)
if reservoir < 0 { 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 // 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 // frame's state changes are rolled back and its state gas is refilled with LIFO
// (EIP-8037); the difference is that the frame's gas_left is consumed rather // mechanism. The difference is that the frame's regular gas is consumed rather
// than returned. The portion refilled to RegularGas is therefore burned along // 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. // which equals the reservoir's value at the start of the frame.
func (g GasBudget) ExitHalt() GasBudget { func (g GasBudget) ExitHalt() GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled) reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)

View file

@ -635,7 +635,12 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
value = scope.Stack.pop() value = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) 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. // Apply EIP-150 to the regular gas left after the state charge.
forward := scope.Contract.Gas.RegularGas forward := scope.Contract.Gas.RegularGas
if evm.chainRules.IsEIP150 { if evm.chainRules.IsEIP150 {
@ -646,7 +651,8 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
stackvalue := size stackvalue := size
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation) 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 // Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only // homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must // 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 // Refund the leftover gas back to current frame
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// Refund the state gas of account-creation if creation doesn't happen // Refill the account-creation charge if the create frame failed (reverted,
if evm.GetRules().IsAmsterdam && !creation { // 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) scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
} }
if suberr == ErrExecutionReverted { 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() offset, size = scope.Stack.pop(), scope.Stack.pop()
salt = scope.Stack.pop() salt = scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) 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. // Apply EIP-150 to the regular gas left after the state charge.
forward := scope.Contract.Gas.RegularGas forward := scope.Contract.Gas.RegularGas
forward -= forward / 64 forward -= forward / 64
@ -689,7 +704,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// reuse size int for stackvalue // reuse size int for stackvalue
stackvalue := size stackvalue := size
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) 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. // Push item on the stack based on the returned error.
if suberr != nil { if suberr != nil {
stackvalue.Clear() stackvalue.Clear()
@ -701,8 +716,11 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Refund the leftover gas back to current frame // Refund the leftover gas back to current frame
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// Refund the state gas of account-creation if creation doesn't happen // Refill the account-creation charge if the create frame failed (reverted,
if evm.GetRules().IsAmsterdam && !creation { // 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) scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
} }
if suberr == ErrExecutionReverted { if suberr == ErrExecutionReverted {

View file

@ -77,9 +77,11 @@ type StateDB interface {
AddressInAccessList(addr common.Address) bool AddressInAccessList(addr common.Address) bool
SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk 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 // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform
// even if the feature/fork is not active yet // even if the feature/fork is not active yet
AddAddressToAccessList(addr common.Address) AddAddressToAccessList(addr common.Address)
// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // 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 // even if the feature/fork is not active yet
AddSlotToAccessList(addr common.Address, slot common.Hash) AddSlotToAccessList(addr common.Address, slot common.Hash)

View file

@ -192,7 +192,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack} return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
} }
// for tracing: this gas consumption event is emitted below in the debug section. // 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 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) return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
} }
if dynamicCost.StateGas == 0 { if dynamicCost.StateGas == 0 {
if !contract.Gas.chargeRegularOnly(dynamicCost.RegularGas) { if !contract.Gas.ChargeRegularOnly(dynamicCost.RegularGas) {
return nil, ErrOutOfGas return nil, ErrOutOfGas
} }
} else if !contract.Gas.charge(dynamicCost) { } else if !contract.Gas.charge(dynamicCost) {

View file

@ -68,6 +68,7 @@ var (
pragueInstructionSet = newPragueInstructionSet() pragueInstructionSet = newPragueInstructionSet()
osakaInstructionSet = newOsakaInstructionSet() osakaInstructionSet = newOsakaInstructionSet()
amsterdamInstructionSet = newAmsterdamInstructionSet() amsterdamInstructionSet = newAmsterdamInstructionSet()
bogotaInstructionSet = newBogotaInstructionSet()
) )
// JumpTable contains the EVM opcodes supported at a given fork. // JumpTable contains the EVM opcodes supported at a given fork.
@ -91,6 +92,11 @@ func validate(jt JumpTable) JumpTable {
return jt return jt
} }
func newBogotaInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
return validate(instructionSet)
}
func newVerkleInstructionSet() JumpTable { func newVerkleInstructionSet() JumpTable {
instructionSet := newShanghaiInstructionSet() instructionSet := newShanghaiInstructionSet()
enable4762(&instructionSet) enable4762(&instructionSet)

View file

@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) {
switch { switch {
case rules.IsUBT: case rules.IsUBT:
return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") return newCancunInstructionSet(), errors.New("verkle-fork not defined yet")
case rules.IsBogota:
return newBogotaInstructionSet(), nil
case rules.IsAmsterdam: case rules.IsAmsterdam:
return newAmsterdamInstructionSet(), nil return newAmsterdamInstructionSet(), nil
case rules.IsOsaka: case rules.IsOsaka:

View file

@ -493,7 +493,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat
// EIP-7702 delegation check. // EIP-7702 delegation check.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
if evm.StateDB.AddressInAccessList(target) { if evm.StateDB.AddressInAccessList(target) {
eip7702Cost = params.WarmStorageReadCostEIP2929 eip7702Cost = params.WarmAccountAccessAmsterdam
} else { } else {
evm.StateDB.AddAddressToAccessList(target) evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = coldCost eip7702Cost = coldCost

View file

@ -143,12 +143,16 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
cfg.State.CreateAccount(address) cfg.State.CreateAccount(address)
// set the receiver's (the executing contract) code for execution. // set the receiver's (the executing contract) code for execution.
cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) 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. // Call the code with the given configuration.
ret, result, err := vmenv.Call( ret, result, err := vmenv.Call(
cfg.Origin, cfg.Origin,
common.BytesToAddress([]byte("contract")), common.BytesToAddress([]byte("contract")),
input, input,
vm.NewGasBudget(cfg.GasLimit, 0), vm.NewGasBudget(limit, cfg.GasLimit-limit),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { 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) // - prepare accessList(post-berlin)
// - reset transient storage(eip 1153) // - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) 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. // Call the code with the given configuration.
code, address, result, _, err := vmenv.Create( code, address, result, err := vmenv.Create(
cfg.Origin, cfg.Origin,
input, input,
vm.NewGasBudget(cfg.GasLimit, 0), vm.NewGasBudget(limit, cfg.GasLimit-limit),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { 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) // - reset transient storage(eip 1153)
statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil) 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. // Call the code with the given configuration.
ret, result, err := vmenv.Call( ret, result, err := vmenv.Call(
cfg.Origin, cfg.Origin,
address, address,
input, input,
vm.NewGasBudget(cfg.GasLimit, 0), vm.NewGasBudget(limit, cfg.GasLimit-limit),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {

View file

@ -19,6 +19,7 @@ package eth
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"time" "time"
@ -64,6 +65,24 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header {
} }
func (b *EthAPIBackend) SetHead(number uint64) error { 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() b.eth.handler.downloader.Cancel()
return b.eth.blockchain.SetHead(number) return b.eth.blockchain.SetHead(number)
} }

View file

@ -453,6 +453,7 @@ func (s *Ethereum) Downloader() *downloader.Downloader { return s.handler.downlo
func (s *Ethereum) Synced() bool { return s.handler.synced.Load() } func (s *Ethereum) Synced() bool { return s.handler.synced.Load() }
func (s *Ethereum) SetSynced() { s.handler.enableSyncedFeatures() } func (s *Ethereum) SetSynced() { s.handler.enableSyncedFeatures() }
func (s *Ethereum) ArchiveMode() bool { return s.config.NoPruning } func (s *Ethereum) ArchiveMode() bool { return s.config.NoPruning }
func (s *Ethereum) EngineMaxReorgDepth() uint64 { return s.config.EngineMaxReorgDepth }
// Protocols returns all the currently configured // Protocols returns all the currently configured
// network protocols to start. // network protocols to start.
@ -477,8 +478,8 @@ func (s *Ethereum) Start() error {
// Start the networking layer // Start the networking layer
s.handler.Start(s.p2pServer.MaxPeers) s.handler.Start(s.p2pServer.MaxPeers)
// Start the connection manager // Start the connection manager with inclusion-based peer protection.
s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }) s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }, s.handler.txTracker.GetAllPeerStats)
// Subscribe to chain events for the filterMaps head updater. // Subscribe to chain events for the filterMaps head updater.
s.fmHeadSub = s.blockchain.SubscribeChainEvent(s.fmHeadEventCh) s.fmHeadSub = s.blockchain.SubscribeChainEvent(s.fmHeadEventCh)
@ -604,6 +605,7 @@ func (s *Ethereum) Stop() error {
// Stop all the peer-related stuff first. // Stop all the peer-related stuff first.
s.discmix.Close() s.discmix.Close()
s.dropper.Stop() s.dropper.Stop()
s.handler.txTracker.Stop()
s.handler.Stop() s.handler.Stop()
// Then stop everything else. // Then stop everything else.

View file

@ -83,14 +83,15 @@ const (
// beaconUpdateWarnFrequency is the frequency at which to warn the user that // beaconUpdateWarnFrequency is the frequency at which to warn the user that
// the beacon client is offline. // the beacon client is offline.
beaconUpdateWarnFrequency = 5 * time.Minute beaconUpdateWarnFrequency = 5 * time.Minute
// maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated.
maxReorgDepth = 32
) )
type ConsensusAPI struct { type ConsensusAPI struct {
eth *eth.Ethereum 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 remoteBlocks *headerQueue // Cache of remote payloads received
localBlocks *payloadQueue // Cache of local payloads generated localBlocks *payloadQueue // Cache of local payloads generated
@ -146,6 +147,7 @@ func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI {
} }
api := &ConsensusAPI{ api := &ConsensusAPI{
eth: eth, eth: eth,
maxReorgDepth: eth.EngineMaxReorgDepth(),
remoteBlocks: newHeaderQueue(), remoteBlocks: newHeaderQueue(),
localBlocks: newPayloadQueue(), localBlocks: newPayloadQueue(),
invalidBlocksHits: make(map[common.Hash]int), invalidBlocksHits: make(map[common.Hash]int),
@ -334,9 +336,9 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
return valid(nil), nil return valid(nil), nil
} }
depth := api.eth.BlockChain().CurrentBlock().Number.Uint64() - block.NumberU64() 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) 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() { if !api.eth.Synced() {
log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash) log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash)
@ -867,6 +869,8 @@ func (api *ConsensusAPI) NewPayloadV5(ctx context.Context, params engine.Executa
return invalidStatus, paramsErr("nil executionRequests post-prague") return invalidStatus, paramsErr("nil executionRequests post-prague")
case params.SlotNumber == nil: case params.SlotNumber == nil:
return invalidStatus, paramsErr("nil slotnumber post-amsterdam") 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): case !api.checkFork(params.Timestamp, forks.Amsterdam):
return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads") return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads")
} }
@ -1246,13 +1250,13 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin
return bodies 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. // of block bodies by the engine api.
func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBody { func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBodyV2 {
bodies := make([]*engine.ExecutionPayloadBody, len(hashes)) bodies := make([]*engine.ExecutionPayloadBodyV2, len(hashes))
for i, hash := range hashes { for i, hash := range hashes {
block := api.eth.BlockChain().GetBlockByHash(hash) block := api.eth.BlockChain().GetBlockByHash(hash)
bodies[i] = getBody(block) bodies[i] = getBodyV2(block)
} }
return bodies return bodies
} }
@ -1260,16 +1264,16 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engin
// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range // GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range
// of block bodies by the engine api. // of block bodies by the engine api.
func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { 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. // of block bodies by the engine api.
func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV2, error) {
return api.getBodiesByRange(start, count) 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 { if start == 0 || count == 0 {
return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count))
} }
@ -1282,7 +1286,7 @@ func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engin
if last > current { if last > current {
last = current last = current
} }
bodies := make([]*engine.ExecutionPayloadBody, 0, uint64(count)) bodies := make([]*T, 0, uint64(count))
for i := uint64(start); i <= last; i++ { for i := uint64(start); i <= last; i++ {
block := api.eth.BlockChain().GetBlockByNumber(i) block := api.eth.BlockChain().GetBlockByNumber(i)
bodies = append(bodies, getBody(block)) bodies = append(bodies, getBody(block))
@ -1311,6 +1315,17 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBody {
return &result 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. // convertRequests converts a hex requests slice to plain [][]byte.
func convertRequests(hex []hexutil.Bytes) [][]byte { func convertRequests(hex []hexutil.Bytes) [][]byte {
if hex == nil { if hex == nil {

View file

@ -40,6 +40,7 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool/blobpool" "github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "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"
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
@ -427,8 +428,9 @@ func TestEth2DeepReorg(t *testing.T) {
*/ */
} }
// startEthService creates a full node instance for testing. // startEthService creates a full node instance for testing. The default test
func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { // 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() t.Helper()
n, err := node.New(&node.Config{ n, err := node.New(&node.Config{
@ -449,6 +451,9 @@ func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block)
TrieCleanCache: 256, TrieCleanCache: 256,
Miner: miner.DefaultConfig, Miner: miner.DefaultConfig,
} }
for _, mod := range mods {
mod(ethcfg)
}
ethservice, err := eth.New(n, ethcfg) ethservice, err := eth.New(n, ethcfg)
if err != nil { if err != nil {
t.Fatal("can't create eth service:", err) t.Fatal("can't create eth service:", err)
@ -468,6 +473,54 @@ func startEthService(t testing.TB, genesis *core.Genesis, blocks []*types.Block)
return n, ethservice 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) { func TestFullAPI(t *testing.T) {
genesis, preMergeBlocks := generateMergeChain(10, false) genesis, preMergeBlocks := generateMergeChain(10, false)
n, ethservice := startEthService(t, genesis, preMergeBlocks) n, ethservice := startEthService(t, genesis, preMergeBlocks)
@ -1367,7 +1420,7 @@ func TestGetBlockBodiesByHash(t *testing.T) {
for k, test := range tests { for k, test := range tests {
result := api.GetPayloadBodiesByHashV2(test.hashes) result := api.GetPayloadBodiesByHashV2(test.hashes)
for i, r := range result { 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) t.Fatalf("test %v: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r)
} }
} }
@ -1445,7 +1498,7 @@ func TestGetBlockBodiesByRange(t *testing.T) {
} }
if len(result) == len(test.results) { if len(result) == len(test.results) {
for i, r := range result { 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) t.Fatalf("test %d: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r)
} }
} }
@ -1521,6 +1574,75 @@ func checkEqualBody(a *types.Body, b *engine.ExecutionPayloadBody) error {
return nil 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) { func TestBlockToPayloadWithBlobs(t *testing.T) {
header := types.Header{} header := types.Header{}
var txs []*types.Transaction var txs []*types.Transaction

View file

@ -74,7 +74,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(ctx context.Context, upd
return engine.STATUS_INVALID, attributesErr("missing withdrawals") return engine.STATUS_INVALID, attributesErr("missing withdrawals")
case params.BeaconRoot == nil: case params.BeaconRoot == nil:
return engine.STATUS_INVALID, attributesErr("missing beacon root") 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") 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") return invalidStatus, paramsErr("nil beaconRoot post-cancun")
case executionRequests == nil: case executionRequests == nil:
return invalidStatus, paramsErr("nil executionRequests post-prague") 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") return invalidStatus, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads")
} }
requests := convertRequests(executionRequests) 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") return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun")
case executionRequests == nil: case executionRequests == nil:
return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague") 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") return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads")
} }
requests := convertRequests(executionRequests) requests := convertRequests(executionRequests)

View file

@ -333,8 +333,8 @@ func (d *Downloader) fetchHeaders(from uint64) error {
return errNoPivotHeader return errNoPivotHeader
} }
// Write out the pivot into the database so a rollback beyond // Write out the pivot into the database so a rollback beyond
// it will reenable snap sync and update the state root that // it can be detected, and update the state root that the
// the state syncer will be downloading // state syncer will be downloading
rawdb.WriteLastPivotNumber(d.stateDB, d.pivotHeader.Number.Uint64()) rawdb.WriteLastPivotNumber(d.stateDB, d.pivotHeader.Number.Uint64())
} }
} }

View file

@ -542,8 +542,8 @@ func (d *Downloader) syncToHead() (err error) {
if pivotNumber <= origin { if pivotNumber <= origin {
origin = pivotNumber - 1 origin = pivotNumber - 1
} }
// Write out the pivot into the database so a rollback beyond it will // Write out the pivot into the database so a rollback beyond it
// reenable snap sync // can be detected
rawdb.WriteLastPivotNumber(d.stateDB, pivotNumber) rawdb.WriteLastPivotNumber(d.stateDB, pivotNumber)
} }
} }

View file

@ -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 // 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. // block is ahead, so snap sync was enabled for this node at a certain point.
// The scenarios where this can happen is // The scenarios where this can happen is
// * if the user manually (or via a bad block) rolled back a snap sync node // * if an internal reset (a fork config change or corruption recovery)
// below the sync point. // rolled a snap sync node back below the sync point.
// * the last snap sync is not finished while user specifies a full sync this // * 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. // time. But we don't have any recent state for full sync.
// In these cases however it's safe to reenable snap 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 { if report {
logger = log.Info logger = log.Info
} }
// We are probably in full sync, but we might have rewound to before the // We are probably in full sync, but an internal reset may have rewound the
// snap sync pivot, check if we should re-enable snap sync. // chain below the snap sync pivot, check if we should re-enable snap sync.
head := m.chain.CurrentBlock() head := m.chain.CurrentBlock()
if pivot := rawdb.ReadLastPivotNumber(m.disk); pivot != nil { if pivot := rawdb.ReadLastPivotNumber(m.disk); pivot != nil {
if head.Number.Uint64() < *pivot { if head.Number.Uint64() < *pivot {

View file

@ -17,6 +17,7 @@
package eth package eth
import ( import (
"cmp"
mrand "math/rand" mrand "math/rand"
"slices" "slices"
"sync" "sync"
@ -24,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "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/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
@ -40,6 +42,10 @@ const (
// dropping when no more peers can be added. Larger numbers result in more // dropping when no more peers can be added. Larger numbers result in more
// aggressive drop behavior. // aggressive drop behavior.
peerDropThreshold = 0 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 ( var (
@ -47,18 +53,55 @@ var (
droppedInbound = metrics.NewRegisteredMeter("eth/dropper/inbound", nil) droppedInbound = metrics.NewRegisteredMeter("eth/dropper/inbound", nil)
// droppedOutbound is the number of outbound peers dropped // droppedOutbound is the number of outbound peers dropped
droppedOutbound = metrics.NewRegisteredMeter("eth/dropper/outbound", nil) 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: // Callback type to get per-peer inclusion statistics.
// - during sync the Downloader handles peer connections, so dropper is disabled type getPeerStatsFunc func() map[string]txtracker.PeerStats
// - if not syncing and the peer count is close to the limit, it drops peers
// randomly every peerDropInterval to make space for new peers // protectionCategory defines a peer scoring function and the fraction of peers
// - peers are dropped separately from the inbound pool and from the dialed pool // 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.01.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 37 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 { type dropper struct {
maxDialPeers int // maximum number of dialed peers maxDialPeers int // maximum number of dialed peers
maxInboundPeers int // maximum number of inbound peers maxInboundPeers int // maximum number of inbound peers
peersFunc getPeersFunc peersFunc getPeersFunc
syncingFunc getSyncingFunc syncingFunc getSyncingFunc
peerStatsFunc getPeerStatsFunc // optional: inclusion stats for protection
// peerDropTimer introduces churn if we are close to limit capacity. // peerDropTimer introduces churn if we are close to limit capacity.
// We handle Dialed and Inbound connections separately // We handle Dialed and Inbound connections separately
@ -88,10 +131,12 @@ func newDropper(maxDialPeers, maxInboundPeers int) *dropper {
return cm return cm
} }
// Start the dropper. // Start the dropper. peerStatsFunc is optional (nil disables inclusion
func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc) { // protection).
func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc, peerStatsFunc getPeerStatsFunc) {
cm.peersFunc = srv.Peers cm.peersFunc = srv.Peers
cm.syncingFunc = syncingFunc cm.syncingFunc = syncingFunc
cm.peerStatsFunc = peerStatsFunc
cm.wg.Add(1) cm.wg.Add(1)
go cm.loop() go cm.loop()
} }
@ -114,30 +159,101 @@ func (cm *dropper) dropRandomPeer() bool {
} }
numDialed := len(peers) - numInbound 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 { 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() || return p.Trusted() || p.StaticDialed() ||
p.Lifetime() < mclock.AbsTime(doNotDropBefore) || p.Lifetime() < mclock.AbsTime(doNotDropBefore) ||
(p.DynDialed() && cm.maxDialPeers-numDialed > peerDropThreshold) || (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) droppable := slices.DeleteFunc(peers, selectDoNotDrop)
if len(droppable) > 0 { if len(droppable) == 0 {
p := droppable[mrand.Intn(len(droppable))] dropSkipped.Mark(1)
log.Debug("Dropping random peer", "inbound", p.Inbound(), return false
"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
} }
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. // randomDuration generates a random duration between min and max.

234
eth/dropper_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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))
}
}

View file

@ -75,6 +75,7 @@ var Defaults = Config{
RPCEVMTimeout: 5 * time.Second, RPCEVMTimeout: 5 * time.Second,
GPO: FullNodeGPO, GPO: FullNodeGPO,
RPCTxFeeCap: 1, // 1 ether RPCTxFeeCap: 1, // 1 ether
EngineMaxReorgDepth: 32,
TxSyncDefaultTimeout: 20 * time.Second, TxSyncDefaultTimeout: 20 * time.Second,
TxSyncMaxTimeout: 1 * time.Minute, TxSyncMaxTimeout: 1 * time.Minute,
SlowBlockThreshold: -1, // Disabled by default; set via --debug.logslowblock flag SlowBlockThreshold: -1, // Disabled by default; set via --debug.logslowblock flag
@ -203,6 +204,11 @@ type Config struct {
// send-transaction variants. The unit is ether. // send-transaction variants. The unit is ether.
RPCTxFeeCap float64 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 (TODO: remove after the fork)
OverrideOsaka *uint64 `toml:",omitempty"` OverrideOsaka *uint64 `toml:",omitempty"`

View file

@ -63,6 +63,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
RPCGasCap uint64 RPCGasCap uint64
RPCEVMTimeout time.Duration RPCEVMTimeout time.Duration
RPCTxFeeCap float64 RPCTxFeeCap float64
EngineMaxReorgDepth uint64 `toml:",omitempty"`
OverrideOsaka *uint64 `toml:",omitempty"` OverrideOsaka *uint64 `toml:",omitempty"`
OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideAmsterdam *uint64 `toml:",omitempty"`
OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"`
@ -119,6 +120,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.RPCGasCap = c.RPCGasCap enc.RPCGasCap = c.RPCGasCap
enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCEVMTimeout = c.RPCEVMTimeout
enc.RPCTxFeeCap = c.RPCTxFeeCap enc.RPCTxFeeCap = c.RPCTxFeeCap
enc.EngineMaxReorgDepth = c.EngineMaxReorgDepth
enc.OverrideOsaka = c.OverrideOsaka enc.OverrideOsaka = c.OverrideOsaka
enc.OverrideAmsterdam = c.OverrideAmsterdam enc.OverrideAmsterdam = c.OverrideAmsterdam
enc.OverrideBPO1 = c.OverrideBPO1 enc.OverrideBPO1 = c.OverrideBPO1
@ -179,6 +181,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
RPCGasCap *uint64 RPCGasCap *uint64
RPCEVMTimeout *time.Duration RPCEVMTimeout *time.Duration
RPCTxFeeCap *float64 RPCTxFeeCap *float64
EngineMaxReorgDepth *uint64 `toml:",omitempty"`
OverrideOsaka *uint64 `toml:",omitempty"` OverrideOsaka *uint64 `toml:",omitempty"`
OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideAmsterdam *uint64 `toml:",omitempty"`
OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"`
@ -330,6 +333,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.RPCTxFeeCap != nil { if dec.RPCTxFeeCap != nil {
c.RPCTxFeeCap = *dec.RPCTxFeeCap c.RPCTxFeeCap = *dec.RPCTxFeeCap
} }
if dec.EngineMaxReorgDepth != nil {
c.EngineMaxReorgDepth = *dec.EngineMaxReorgDepth
}
if dec.OverrideOsaka != nil { if dec.OverrideOsaka != nil {
c.OverrideOsaka = dec.OverrideOsaka c.OverrideOsaka = dec.OverrideOsaka
} }

View file

@ -183,10 +183,11 @@ type TxFetcher struct {
alternates map[common.Hash]map[string]struct{} // In-flight transaction alternate origins if retrieval fails alternates map[common.Hash]map[string]struct{} // In-flight transaction alternate origins if retrieval fails
// Callbacks // Callbacks
validateMeta func(common.Hash, byte) error // Validate a tx metadata based on the local txpool 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 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 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 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
buffer *blobpool.BlobBuffer buffer *blobpool.BlobBuffer
@ -200,15 +201,15 @@ type TxFetcher struct {
// based on hash announcements. // based on hash announcements.
// Chain can be nil to disable on-chain checks. // 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, 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), buffer *blobpool.BlobBuffer) *TxFetcher { dropPeer func(string), onAccepted func(string, []common.Hash), buffer *blobpool.BlobBuffer) *TxFetcher {
return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, buffer, mclock.System{}, time.Now, nil) return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, onAccepted, buffer, mclock.System{}, time.Now, nil)
} }
// NewTxFetcherForTests is a testing method to mock out the realtime clock with // NewTxFetcherForTests is a testing method to mock out the realtime clock with
// a simulated version and the internal randomness with a deterministic one. // a simulated version and the internal randomness with a deterministic one.
// Chain can be nil to disable on-chain checks. // Chain can be nil to disable on-chain checks.
func NewTxFetcherForTests( 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),
buffer *blobpool.BlobBuffer, clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher { buffer *blobpool.BlobBuffer, clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher {
return &TxFetcher{ return &TxFetcher{
notify: make(chan *txAnnounce), notify: make(chan *txAnnounce),
@ -231,6 +232,7 @@ func NewTxFetcherForTests(
fetchTxs: fetchTxs, fetchTxs: fetchTxs,
dropPeer: dropPeer, dropPeer: dropPeer,
buffer: buffer, buffer: buffer,
onAccepted: onAccepted,
clock: clock, clock: clock,
realTime: realTime, realTime: realTime,
rand: rand, rand: rand,
@ -387,7 +389,11 @@ func (f *TxFetcher) Enqueue(peer string, version uint, txs []*types.Transaction,
for j := range batch { for j := range batch {
hashes[j] = batch[j].Hash() hashes[j] = batch[j].Hash()
} }
var accepted []common.Hash
for j, err := range errs { for j, err := range errs {
if err == nil {
accepted = append(accepted, batch[j].Hash())
}
if errors.Is(err, txpool.ErrKZGVerificationError) || errors.Is(err, txpool.ErrSidecarFormatError) { if errors.Is(err, txpool.ErrKZGVerificationError) || errors.Is(err, txpool.ErrSidecarFormatError) {
// KZG verification failed, terminate transaction processing immediately. // KZG verification failed, terminate transaction processing immediately.
// Since KZG verification is computationally expensive, this acts as a // Since KZG verification is computationally expensive, this acts as a
@ -406,6 +412,11 @@ func (f *TxFetcher) Enqueue(peer string, version uint, txs []*types.Transaction,
} }
} }
otherreject := f.handleAddErrors(hashes, errs, metrics) otherreject := f.handleAddErrors(hashes, errs, metrics)
// 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 'other reject' is >25% of the deliveries in any batch, sleep a bit
// to throttle the misbehaving peer. // to throttle the misbehaving peer.
if otherreject > int64((len(hashes)+3)/4) { if otherreject > int64((len(hashes)+3)/4) {

View file

@ -111,6 +111,7 @@ func newTestTxFetcher() *TxFetcher {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
nil, nil,
nil,
newTestBlobBuffer(), newTestBlobBuffer(),
) )
} }
@ -2217,6 +2218,7 @@ func TestTransactionForgotten(t *testing.T) {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
func(string) {}, func(string) {},
nil,
newTestBlobBuffer(), newTestBlobBuffer(),
mockClock, mockClock,
mockTime, mockTime,

View file

@ -40,6 +40,7 @@ import (
"github.com/ethereum/go-ethereum/eth/fetcher" "github.com/ethereum/go-ethereum/eth/fetcher"
"github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/eth/protocols/snap" "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/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -140,6 +141,7 @@ type handler struct {
downloader *downloader.Downloader downloader *downloader.Downloader
txFetcher *fetcher.TxFetcher txFetcher *fetcher.TxFetcher
blobFetcher *fetcher.BlobFetcher blobFetcher *fetcher.BlobFetcher
txTracker *txtracker.Tracker
peers *peerSet peers *peerSet
txBroadcastKey [16]byte txBroadcastKey [16]byte
@ -208,7 +210,8 @@ func newHandler(config *handlerConfig) (*handler, error) {
} }
return nil return nil
} }
h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer, blobBuffer) h.txTracker = txtracker.New()
h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer, h.txTracker.NotifyAccepted, blobBuffer)
// Construct the blob fetcher for cell-based blob data availability // Construct the blob fetcher for cell-based blob data availability
blobCallbacks := fetcher.BlobFetcherFunctions{ blobCallbacks := fetcher.BlobFetcherFunctions{
@ -453,6 +456,7 @@ func (h *handler) unregisterPeer(id string) {
h.downloader.UnregisterPeer(id) h.downloader.UnregisterPeer(id)
h.txFetcher.Drop(id) h.txFetcher.Drop(id)
h.blobFetcher.Drop(id) h.blobFetcher.Drop(id)
h.txTracker.NotifyPeerDrop(id)
if err := h.peers.unregisterPeer(id); err != nil { if err := h.peers.unregisterPeer(id); err != nil {
logger.Error("Ethereum peer removal failed", "err", err) logger.Error("Ethereum peer removal failed", "err", err)
@ -477,6 +481,9 @@ func (h *handler) Start(maxPeers int) {
h.txFetcher.Start() h.txFetcher.Start()
h.blobFetcher.Start() h.blobFetcher.Start()
// Start the transaction tracker (records tx deliveries, credits peer inclusions).
h.txTracker.Start(h.chain)
// start peer handler tracker // start peer handler tracker
h.wg.Add(1) h.wg.Add(1)
go h.protoTracker() go h.protoTracker()

View file

@ -798,11 +798,22 @@ func catchUpExceedsRetention(prev, curr *types.Header) bool {
// diffs to roll flat state forward. // diffs to roll flat state forward.
func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error { func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error {
s.lock.RLock() s.lock.RLock()
from := s.pivot.Number.Uint64() + 1 prev := s.pivot
to := target.Number.Uint64()
s.lock.RUnlock() 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) 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.lock.Lock()
s.accessListTotal = to - from + 1 s.accessListTotal = to - from + 1
s.accessListSynced = 0 s.accessListSynced = 0
@ -819,26 +830,26 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error {
if end > to { if end > to {
end = to end = to
} }
// Collect block hashes and headers for this window. // Collect the headers for this window.
var ( var (
hashes = make([]common.Hash, 0, end-start+1) winHashes = hashes[start-from : end-from+1]
headers = make(map[common.Hash]*types.Header, end-start+1) headers = make(map[common.Hash]*types.Header, len(winHashes))
) )
for num := start; num <= end; num++ { for i, hash := range winHashes {
hash := rawdb.ReadCanonicalHash(s.db, num) num := start + uint64(i)
if hash == (common.Hash{}) { if num == to {
return fmt.Errorf("missing canonical hash for block %d during catch-up", num) headers[hash] = target
continue
} }
header := rawdb.ReadHeader(s.db, hash, num) header := s.gapHeader(num, hash)
if header == nil { if header == nil {
return fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, hash) return fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, hash)
} }
hashes = append(hashes, hash)
headers[hash] = header headers[hash] = header
} }
// Fetch this window's BALs from peers. // Fetch this window's BALs from peers.
rawBALs, err := s.fetchAccessLists(hashes, headers, cancel) rawBALs, err := s.fetchAccessLists(winHashes, headers, cancel)
if err != nil { if err != nil {
return err return err
} }
@ -851,7 +862,7 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error {
default: default:
} }
num := start + uint64(i) num := start + uint64(i)
hash := hashes[i] hash := winHashes[i]
// Decode the raw RLP into a BAL. // Decode the raw RLP into a BAL.
var ( var (
@ -883,6 +894,44 @@ func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error {
return nil 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 // fetchAccessLists fetches BALs for the given block hashes from
// remote peers. It runs its own event loop to assign requests // remote peers. It runs its own event loop to assign requests
// to idle peers and process responses asynchronously. Each BAL is verified // to idle peers and process responses asynchronously. Each BAL is verified
@ -914,6 +963,7 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has
accessListReqFails = make(chan *accessListRequest) accessListReqFails = make(chan *accessListRequest)
accessListResps = make(chan *accessListResponse) accessListResps = make(chan *accessListResponse)
lastStallLog = time.Now() lastStallLog = time.Now()
lastFetched = 0
) )
for len(fetched) < len(hashes) { for len(fetched) < len(hashes) {
if err := s.checkAccessListProgress(pending, refused); err != nil { if err := s.checkAccessListProgress(pending, refused); err != nil {
@ -951,7 +1001,8 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has
s.processAccessListResponse(res, headers, pending, fetched, refused) s.processAccessListResponse(res, headers, pending, fetched, refused)
} }
s.lock.Lock() s.lock.Lock()
s.accessListSynced += uint64(len(fetched)) s.accessListSynced += uint64(len(fetched) - lastFetched)
lastFetched = len(fetched)
s.refreshProgressLocked() s.refreshProgressLocked()
s.lock.Unlock() s.lock.Unlock()
} }

View file

@ -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 // 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 // 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) blocks := make([]balBlock, 3)
parent := pivotAHeader.Hash()
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
blockNum := numA + uint64(i) + 1 blockNum := numA + uint64(i) + 1
target := goodAddr target := goodAddr
@ -1819,7 +1822,8 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
} }
balHash := b.Hash() balHash := b.Hash()
header := &types.Header{ 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, BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
BlobGasUsed: &zero, ExcessBlobGas: &zero, BlobGasUsed: &zero, ExcessBlobGas: &zero,
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash, ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
@ -1828,6 +1832,7 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
rawdb.WriteHeader(db, header) rawdb.WriteHeader(db, header)
rawdb.WriteCanonicalHash(db, header.Hash(), blockNum) rawdb.WriteCanonicalHash(db, header.Hash(), blockNum)
blocks[i] = balBlock{header: header, bal: buf.Bytes()} blocks[i] = balBlock{header: header, bal: buf.Bytes()}
parent = header.Hash()
} }
// First sync: complete sync to A so persisted state has pivot=A, // 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) rawdb.WriteCanonicalHash(db, pivotA.Hash(), numA)
// Build a 5-block gap, each block bumping targetAddr's balance. The last // 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 const gap = 5
var ( var (
lastHeader *types.Header lastHeader *types.Header
lastBalance *uint256.Int lastBalance *uint256.Int
balsByHash = make(map[common.Hash]rlp.RawValue, gap) balsByHash = make(map[common.Hash]rlp.RawValue, gap)
parent = pivotA.Hash()
) )
for i := 0; i < gap; i++ { for i := 0; i < gap; i++ {
blockNum := numA + uint64(i) + 1 blockNum := numA + uint64(i) + 1
@ -1952,7 +1960,8 @@ func testCatchUpWindowed(t *testing.T, scheme string) {
} }
balHash := b.Hash() balHash := b.Hash()
header := &types.Header{ 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, BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
BlobGasUsed: &zero, ExcessBlobGas: &zero, BlobGasUsed: &zero, ExcessBlobGas: &zero,
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash, ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
@ -1962,6 +1971,7 @@ func testCatchUpWindowed(t *testing.T, scheme string) {
rawdb.WriteCanonicalHash(db, header.Hash(), blockNum) rawdb.WriteCanonicalHash(db, header.Hash(), blockNum)
balsByHash[header.Hash()] = buf.Bytes() balsByHash[header.Hash()] = buf.Bytes()
lastHeader, lastBalance = header, balance lastHeader, lastBalance = header, balance
parent = header.Hash()
} }
// Seed sync to A: persisted state ends with pivot=A and full flat state. // 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) rawdb.WriteCanonicalHash(db, hdrA.Hash(), numA)
hdrB := &types.Header{ 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, BaseFee: common.Big0, WithdrawalsHash: &emptyH,
BlobGasUsed: &zero, ExcessBlobGas: &zero, BlobGasUsed: &zero, ExcessBlobGas: &zero,
ParentBeaconRoot: &emptyH, RequestsHash: &emptyH, ParentBeaconRoot: &emptyH, RequestsHash: &emptyH,

View file

@ -51,6 +51,19 @@ type Config struct {
Overrides *params.ChainConfig `json:"overrides,omitempty"` 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 //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 // 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 writer io.Writer // If set, the logger will stream instead of store logs
logs []json.RawMessage // buffer of json-encoded 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 interrupt atomic.Bool // Atomic flag to signal execution interruption
reason atomic.Pointer[error] // Reason for the interruption, populated by Stop reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
@ -237,7 +250,7 @@ type StructLogger struct {
// NewStreamingStructLogger returns a new streaming logger. // NewStreamingStructLogger returns a new streaming logger.
func NewStreamingStructLogger(cfg *Config, writer io.Writer) *StructLogger { func NewStreamingStructLogger(cfg *Config, writer io.Writer) *StructLogger {
l := NewStructLogger(cfg) l := NewStructLogger(cfg)
l.writer = writer l.writer = &countingWriter{w: writer}
return l return l
} }
@ -334,6 +347,7 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
return return
} }
log.Write(l.writer) log.Write(l.writer)
l.resultSize = l.writer.(*countingWriter).n
} }
// OnExit is called a call frame finishes processing. // OnExit is called a call frame finishes processing.

View file

@ -59,12 +59,14 @@ type jsonLogger struct {
cfg *Config cfg *Config
env *tracing.VMContext env *tracing.VMContext
hooks *tracing.Hooks hooks *tracing.Hooks
written *countingWriter
} }
// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects // NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects
// into the provided stream. // into the provided stream.
func NewJSONLogger(cfg *Config, writer io.Writer) *tracing.Hooks { 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 { if l.cfg == nil {
l.cfg = &Config{} 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 // 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. // into the provided stream. It also includes call frames in the output.
func NewJSONLoggerWithCallFrames(cfg *Config, writer io.Writer) *tracing.Hooks { 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 { if l.cfg == nil {
l.cfg = &Config{} 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) { 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() memory := scope.MemoryData()
stack := scope.StackData() stack := scope.StackData()

320
eth/txtracker/tracker.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
// 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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

2
go.mod
View file

@ -22,7 +22,7 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 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/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab
github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a
github.com/fatih/color v1.16.0 github.com/fatih/color v1.16.0

4
go.sum
View file

@ -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/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 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 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.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58=
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/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 h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= 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= github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a h1:eIFUceK3U/z9UV0D/kAI6cxA27eH7MPqt2ks7fbzj/k=

View file

@ -195,6 +195,12 @@ func (args *TransactionArgs) setFeeDefaults(ctx context.Context, b Backend, head
if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") 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. // 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 // 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 // other tx values. See https://github.com/ethereum/go-ethereum/pull/23274

View file

@ -58,6 +58,7 @@ func TestSetFeeDefaults(t *testing.T) {
fortytwo = (*hexutil.Big)(big.NewInt(42)) 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())) 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}}}} al = &types.AccessList{types.AccessTuple{Address: common.Address{0xaa}, StorageKeys: []common.Hash{{0x01}}}}
authList = []types.SetCodeAuthorization{{Address: common.Address{0xbb}}}
) )
tests := []test{ tests := []test{
@ -208,6 +209,13 @@ func TestSetFeeDefaults(t *testing.T) {
nil, nil,
errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified"), 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 // EIP-4844
{ {
"set gas price and maxFee for blob transaction", "set gas price and maxFee for blob transaction",

View file

@ -22,6 +22,7 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"slices"
"sync" "sync"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -76,7 +77,7 @@ func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
copy(records[:], h.buf[:]) copy(records[:], h.buf[:])
return &bufHandler{ return &bufHandler{
buf: records, buf: records,
attrs: append(h.attrs, attrs...), attrs: append(slices.Clone(h.attrs), attrs...),
level: h.level, level: h.level,
} }
} }
@ -186,7 +187,7 @@ func (h *bufHandler) terminalFormat(r slog.Record) string {
return true 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) fmt.Fprintf(buf, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Message)
if length := len(r.Message); length < 40 { if length := len(r.Message); length < 40 {

View file

@ -51,8 +51,8 @@ var (
// Users can specify the maximum number of blobs per block if necessary. // Users can specify the maximum number of blobs per block if necessary.
func (miner *Miner) maxBlobsPerBlock(time uint64) int { func (miner *Miner) maxBlobsPerBlock(time uint64) int {
maxBlobs := eip4844.MaxBlobsPerBlock(miner.chainConfig, time) maxBlobs := eip4844.MaxBlobsPerBlock(miner.chainConfig, time)
if miner.config.MaxBlobsPerBlock != 0 { if configured := miner.config.MaxBlobsPerBlock; configured != 0 && configured < maxBlobs {
maxBlobs = miner.config.MaxBlobsPerBlock maxBlobs = configured
} }
return maxBlobs return maxBlobs
} }

View file

@ -64,6 +64,7 @@ var (
OsakaTime: newUint64(1764798551), OsakaTime: newUint64(1764798551),
BPO1Time: newUint64(1765290071), BPO1Time: newUint64(1765290071),
BPO2Time: newUint64(1767747671), BPO2Time: newUint64(1767747671),
BogotaTime: nil,
DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"), DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
@ -99,6 +100,7 @@ var (
OsakaTime: newUint64(1759308480), OsakaTime: newUint64(1759308480),
BPO1Time: newUint64(1759800000), BPO1Time: newUint64(1759800000),
BPO2Time: newUint64(1760389824), BPO2Time: newUint64(1760389824),
BogotaTime: nil,
DepositContractAddress: common.HexToAddress("0x4242424242424242424242424242424242424242"), DepositContractAddress: common.HexToAddress("0x4242424242424242424242424242424242424242"),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
@ -134,6 +136,7 @@ var (
OsakaTime: newUint64(1760427360), OsakaTime: newUint64(1760427360),
BPO1Time: newUint64(1761017184), BPO1Time: newUint64(1761017184),
BPO2Time: newUint64(1761607008), BPO2Time: newUint64(1761607008),
BogotaTime: nil,
DepositContractAddress: common.HexToAddress("0x7f02c3e3c98b133055b8b348b2ac625669ed295d"), DepositContractAddress: common.HexToAddress("0x7f02c3e3c98b133055b8b348b2ac625669ed295d"),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
@ -169,6 +172,7 @@ var (
OsakaTime: newUint64(1761677592), OsakaTime: newUint64(1761677592),
BPO1Time: newUint64(1762365720), BPO1Time: newUint64(1762365720),
BPO2Time: newUint64(1762955544), BPO2Time: newUint64(1762955544),
BogotaTime: nil,
DepositContractAddress: common.HexToAddress("0x00000000219ab540356cBB839Cbe05303d7705Fa"), DepositContractAddress: common.HexToAddress("0x00000000219ab540356cBB839Cbe05303d7705Fa"),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
@ -203,6 +207,7 @@ var (
CancunTime: nil, CancunTime: nil,
PragueTime: nil, PragueTime: nil,
OsakaTime: nil, OsakaTime: nil,
BogotaTime: nil,
UBTTime: nil, UBTTime: nil,
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
Clique: nil, Clique: nil,
@ -228,6 +233,7 @@ var (
TerminalTotalDifficulty: big.NewInt(0), TerminalTotalDifficulty: big.NewInt(0),
PragueTime: newUint64(0), PragueTime: newUint64(0),
OsakaTime: newUint64(0), OsakaTime: newUint64(0),
BogotaTime: newUint64(0),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
Cancun: DefaultCancunBlobConfig, Cancun: DefaultCancunBlobConfig,
Prague: DefaultPragueBlobConfig, Prague: DefaultPragueBlobConfig,
@ -258,6 +264,7 @@ var (
CancunTime: nil, CancunTime: nil,
PragueTime: nil, PragueTime: nil,
OsakaTime: nil, OsakaTime: nil,
BogotaTime: nil,
UBTTime: nil, UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64), TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: nil, Ethash: nil,
@ -288,6 +295,7 @@ var (
CancunTime: nil, CancunTime: nil,
PragueTime: nil, PragueTime: nil,
OsakaTime: nil, OsakaTime: nil,
BogotaTime: nil,
UBTTime: nil, UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64), TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
@ -318,6 +326,7 @@ var (
CancunTime: newUint64(0), CancunTime: newUint64(0),
PragueTime: newUint64(0), PragueTime: newUint64(0),
OsakaTime: newUint64(0), OsakaTime: newUint64(0),
BogotaTime: nil,
UBTTime: nil, UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(0), TerminalTotalDifficulty: big.NewInt(0),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
@ -352,6 +361,7 @@ var (
CancunTime: nil, CancunTime: nil,
PragueTime: nil, PragueTime: nil,
OsakaTime: nil, OsakaTime: nil,
BogotaTime: nil,
UBTTime: nil, UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64), TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: new(EthashConfig), 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) 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) 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) 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) UBTTime *uint64 `json:"ubtTime,omitempty"` // UBT switch time (nil = no fork, 0 = already on UBT)
// TerminalTotalDifficulty is the amount of total difficulty reached by // TerminalTotalDifficulty is the amount of total difficulty reached by
@ -582,6 +593,9 @@ func (c *ChainConfig) String() string {
if c.AmsterdamTime != nil { if c.AmsterdamTime != nil {
result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime) result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime)
} }
if c.BogotaTime != nil {
result += fmt.Sprintf(", BogotaTime: %v", *c.BogotaTime)
}
if c.UBTTime != nil { if c.UBTTime != nil {
result += fmt.Sprintf(", UBTTime: %v", *c.UBTTime) result += fmt.Sprintf(", UBTTime: %v", *c.UBTTime)
} }
@ -677,6 +691,9 @@ func (c *ChainConfig) Description() string {
if c.AmsterdamTime != nil { if c.AmsterdamTime != nil {
banner += fmt.Sprintf(" - Amsterdam: @%-10v\n", *c.AmsterdamTime) banner += fmt.Sprintf(" - Amsterdam: @%-10v\n", *c.AmsterdamTime)
} }
if c.BogotaTime != nil {
banner += fmt.Sprintf(" - Bogota: @%-10v\n", *c.BogotaTime)
}
if c.UBTTime != nil { if c.UBTTime != nil {
banner += fmt.Sprintf(" - UBT: @%-10v\n", *c.UBTTime) 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) 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. // IsUBT returns whether time is either equal to the Verkle fork time or greater.
func (c *ChainConfig) IsUBT(num *big.Int, time uint64) bool { func (c *ChainConfig) IsUBT(num *big.Int, time uint64) bool {
return c.IsLondon(num) && isTimestampForked(c.UBTTime, time) 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: "bpo4", timestamp: c.BPO4Time, optional: true},
{name: "bpo5", timestamp: c.BPO5Time, optional: true}, {name: "bpo5", timestamp: c.BPO5Time, optional: true},
{name: "amsterdam", timestamp: c.AmsterdamTime, optional: true}, {name: "amsterdam", timestamp: c.AmsterdamTime, optional: true},
{name: "bogota", timestamp: c.BogotaTime, optional: true},
} { } {
if lastFork.name != "" { if lastFork.name != "" {
switch { switch {
@ -1111,6 +1134,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int,
if isForkTimestampIncompatible(c.AmsterdamTime, newcfg.AmsterdamTime, headTimestamp) { if isForkTimestampIncompatible(c.AmsterdamTime, newcfg.AmsterdamTime, headTimestamp) {
return newTimestampCompatError("Amsterdam fork timestamp", c.AmsterdamTime, newcfg.AmsterdamTime) 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 return nil
} }
@ -1130,6 +1156,8 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork {
london := c.LondonBlock london := c.LondonBlock
switch { switch {
case c.IsBogota(london, time):
return forks.Bogota
case c.IsAmsterdam(london, time): case c.IsAmsterdam(london, time):
return forks.Amsterdam return forks.Amsterdam
case c.IsBPO5(london, time): 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. // the fork isn't defined or isn't a time-based fork.
func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 {
switch { switch {
case fork == forks.Bogota:
return c.BogotaTime
case fork == forks.Amsterdam:
return c.AmsterdamTime
case fork == forks.BPO5: case fork == forks.BPO5:
return c.BPO5Time return c.BPO5Time
case fork == forks.BPO4: case fork == forks.BPO4:
@ -1231,8 +1263,6 @@ func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 {
return c.CancunTime return c.CancunTime
case fork == forks.Shanghai: case fork == forks.Shanghai:
return c.ShanghaiTime return c.ShanghaiTime
case fork == forks.Amsterdam:
return c.AmsterdamTime
default: default:
return nil return nil
} }
@ -1378,7 +1408,7 @@ type Rules struct {
IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
IsBerlin, IsLondon bool IsBerlin, IsLondon bool
IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool
IsAmsterdam, IsUBT bool IsAmsterdam, IsBogota, IsUBT bool
} }
// Rules ensures c's ChainID is not nil. // 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), IsPrague: isMerge && c.IsPrague(num, timestamp),
IsOsaka: isMerge && c.IsOsaka(num, timestamp), IsOsaka: isMerge && c.IsOsaka(num, timestamp),
IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp), IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp),
IsBogota: isMerge && c.IsBogota(num, timestamp),
IsUBT: isUBT, IsUBT: isUBT,
IsEIP4762: isUBT, IsEIP4762: isUBT,
} }

View file

@ -46,6 +46,7 @@ const (
BPO4 BPO4
BPO5 BPO5
Amsterdam Amsterdam
Bogota
) )
// String implements fmt.Stringer. // String implements fmt.Stringer.
@ -84,4 +85,5 @@ var forkToString = map[Fork]string{
BPO4: "BPO4", BPO4: "BPO4",
BPO5: "BPO5", BPO5: "BPO5",
Amsterdam: "Amsterdam", Amsterdam: "Amsterdam",
Bogota: "Bogota",
} }

View file

@ -31,8 +31,6 @@ const (
MaxTxGas uint64 = 1 << 24 // Maximum transaction gas limit after eip-7825 (16,777,216). 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. 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. 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. 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. 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 // Which becomes: 5000 - 2100 + 1900 = 4800
SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas
JumpdestGas uint64 = 1 // Once per JUMPDEST operation. JumpdestGas uint64 = 1 // Once per JUMPDEST operation.
EpochDuration uint64 = 30000 // Duration between proof-of-work epochs.
CreateDataGas uint64 = 200 // CreateDataGas uint64 = 200 //
CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack. CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack.
@ -84,7 +81,6 @@ const (
LogGas uint64 = 375 // Per LOG* operation. 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. 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. 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. 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. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
Create2Gas uint64 = 32000 // Once per CREATE2 operation Create2Gas uint64 = 32000 // Once per CREATE2 operation
@ -101,20 +97,27 @@ const (
TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list
TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 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. // EIP-2780: resource-based intrinsic transaction gas.
TxBaseCost2780 uint64 = 12000 TxBaseCost2780 uint64 = 12000
ColdAccountAccess2780 uint64 = 3000 TxValueCost2780 uint64 = 4244
CreateAccess2780 uint64 = 11000 TransferLogCost2780 uint64 = 1756
TxValueCost2780 uint64 = 4244
TransferLogCost2780 uint64 = 1756
// EIP-8038: state-access gas cost update (Amsterdam). // EIP-8038: state-access gas cost update (Amsterdam).
ColdAccountAccessAmsterdam uint64 = 3000 // COLD_ACCOUNT_ACCESS: cold touch of an account 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 AccountWriteAmsterdam uint64 = 8000 // ACCOUNT_WRITE: surcharge for first-time write to an account
CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300) CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300)
ColdStorageAccessAmsterdam uint64 = 3000 // COLD_STORAGE_ACCESS: cold touch of a storage slot 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 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 StorageClearRefundAmsterdam uint64 = 12480 // STORAGE_CLEAR_REFUND: refund for clearing a storage slot
CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS
@ -260,10 +263,10 @@ var (
ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd")
// EIP-8282 - Builder Execution Requests // EIP-8282 - Builder Execution Requests
BuilderDepositAddress = common.HexToAddress("0x0000884d2AA32eAa155F59A2f24eFa73D9008282") BuilderDepositAddress = common.HexToAddress("0x0000BFF46984E3725691FA540A8C7589300D8282")
BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd") BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1461011c575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146102705760015460088111605257506058565b60089003015b601190600182026001905f5b5f821115607f57810190830284830290049160010191906064565b90939004925050503660b814609f57366102705734610270575f5260205ff35b8034106102705760383567ffffffffffffffff1680633b9aca001161027057633b9aca00029034031061027057600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b60035460025480820380604011610131575060405b5f5b8181146101d7578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c816001015353602001816003015481526020018160040154815260200190600501549052600101610133565b91018092146101e957906002556101f4565b90505f6002555f6003555b36610242575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023057600882820111610238575b50505f610264565b0160089003610264565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f60015560b8025ff35b5f5ffd")
BuilderExitAddress = common.HexToAddress("0x000014574A74c805590AFF9499fc7A690f008282") BuilderExitAddress = common.HexToAddress("0x000064D678505AD48F8CCB093BC65613800E8282")
BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd") BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460e1575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101c65760015460028111605157506057565b60029003015b601190600182026001905f5b5f821115607e57810190830284830290049160010191906063565b909390049250505036603014609e57366101c657346101c6575f5260205ff35b34106101c657600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160f5575060105b5f5b81811461012d5782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160f7565b910180921461013f579060025561014a565b90505f6002555f6003555b36610198575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101865760028282011161018e575b50505f6101ba565b01600290036101ba565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f6001556044025ff35b5f5ffd")
// EIP-7997 - Deterministic deployment factory (keyless CREATE2 factory) // EIP-7997 - Deterministic deployment factory (keyless CREATE2 factory)
DeterministicFactoryAddress = common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C") DeterministicFactoryAddress = common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C")

View file

@ -88,14 +88,12 @@ func TestExecutionSpecBlocktests(t *testing.T) {
bt := new(testMatcher) bt := new(testMatcher)
// These tests require us to handle scenarios where a system contract is not deployed at a fork // 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(`.*eip7251_consolidations/contract_deployment/system_contract_deployment\.json`)
bt.skipLoad(".*prague/eip7002_el_triggerable_withdrawals/test_system_contract_deployment.json") bt.skipLoad(`.*eip7002_el_triggerable_withdrawals/contract_deployment/system_contract_deployment\.json`)
// Broken tests // Broken tests
bt.skipLoad(`RevertInCreateInInit`) bt.skipLoad(`.*eip7610_create_collision/initcollision/.*`)
bt.skipLoad(`InitCollisionParis`) bt.skipLoad(`.*eip7610_create_collision/revert_in_create/.*`)
bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
bt.skipLoad(`create2collisionStorageParis`)
bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) {
execBlockTest(t, bt, test) execBlockTest(t, bt, test)

View file

@ -91,7 +91,7 @@ func fuzz(input []byte) int {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
nil, nil,
nil,
blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{ blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{
ValidateTx: func(*types.Transaction) error { return nil }, ValidateTx: func(*types.Transaction) error { return nil },
AddToPool: func(*blobpool.BlobTxForPool) error { return nil }, AddToPool: func(*blobpool.BlobTxForPool) error { return nil },

View file

@ -24,6 +24,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) {
Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"`
ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"`
SlotNumber *math.HexOrDecimal64 `json:"slotNumber" gencodec:"optional"`
} }
var enc stEnv var enc stEnv
enc.Coinbase = common.UnprefixedAddress(s.Coinbase) enc.Coinbase = common.UnprefixedAddress(s.Coinbase)
@ -34,6 +35,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) {
enc.Timestamp = math.HexOrDecimal64(s.Timestamp) enc.Timestamp = math.HexOrDecimal64(s.Timestamp)
enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee) enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee)
enc.ExcessBlobGas = (*math.HexOrDecimal64)(s.ExcessBlobGas) enc.ExcessBlobGas = (*math.HexOrDecimal64)(s.ExcessBlobGas)
enc.SlotNumber = (*math.HexOrDecimal64)(s.SlotNumber)
return json.Marshal(&enc) return json.Marshal(&enc)
} }
@ -48,6 +50,7 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee" gencodec:"optional"`
ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas" gencodec:"optional"`
SlotNumber *math.HexOrDecimal64 `json:"slotNumber" gencodec:"optional"`
} }
var dec stEnv var dec stEnv
if err := json.Unmarshal(input, &dec); err != nil { if err := json.Unmarshal(input, &dec); err != nil {
@ -81,5 +84,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
if dec.ExcessBlobGas != nil { if dec.ExcessBlobGas != nil {
s.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) s.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas)
} }
if dec.SlotNumber != nil {
s.SlotNumber = (*uint64)(dec.SlotNumber)
}
return nil return nil
} }

View file

@ -20,7 +20,7 @@ func (s stTransaction) MarshalJSON() ([]byte, error) {
GasPrice *math.HexOrDecimal256 `json:"gasPrice"` GasPrice *math.HexOrDecimal256 `json:"gasPrice"`
MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"` MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"`
MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"` MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"`
Nonce math.HexOrDecimal64 `json:"nonce"` Nonce *math.HexOrDecimal256 `json:"nonce"`
To string `json:"to"` To string `json:"to"`
Data []string `json:"data"` Data []string `json:"data"`
AccessLists []*types.AccessList `json:"accessLists,omitempty"` AccessLists []*types.AccessList `json:"accessLists,omitempty"`
@ -36,7 +36,7 @@ func (s stTransaction) MarshalJSON() ([]byte, error) {
enc.GasPrice = (*math.HexOrDecimal256)(s.GasPrice) enc.GasPrice = (*math.HexOrDecimal256)(s.GasPrice)
enc.MaxFeePerGas = (*math.HexOrDecimal256)(s.MaxFeePerGas) enc.MaxFeePerGas = (*math.HexOrDecimal256)(s.MaxFeePerGas)
enc.MaxPriorityFeePerGas = (*math.HexOrDecimal256)(s.MaxPriorityFeePerGas) enc.MaxPriorityFeePerGas = (*math.HexOrDecimal256)(s.MaxPriorityFeePerGas)
enc.Nonce = math.HexOrDecimal64(s.Nonce) enc.Nonce = (*math.HexOrDecimal256)(s.Nonce)
enc.To = s.To enc.To = s.To
enc.Data = s.Data enc.Data = s.Data
enc.AccessLists = s.AccessLists enc.AccessLists = s.AccessLists
@ -61,7 +61,7 @@ func (s *stTransaction) UnmarshalJSON(input []byte) error {
GasPrice *math.HexOrDecimal256 `json:"gasPrice"` GasPrice *math.HexOrDecimal256 `json:"gasPrice"`
MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"` MaxFeePerGas *math.HexOrDecimal256 `json:"maxFeePerGas"`
MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"` MaxPriorityFeePerGas *math.HexOrDecimal256 `json:"maxPriorityFeePerGas"`
Nonce *math.HexOrDecimal64 `json:"nonce"` Nonce *math.HexOrDecimal256 `json:"nonce"`
To *string `json:"to"` To *string `json:"to"`
Data []string `json:"data"` Data []string `json:"data"`
AccessLists []*types.AccessList `json:"accessLists,omitempty"` AccessLists []*types.AccessList `json:"accessLists,omitempty"`
@ -87,7 +87,7 @@ func (s *stTransaction) UnmarshalJSON(input []byte) error {
s.MaxPriorityFeePerGas = (*big.Int)(dec.MaxPriorityFeePerGas) s.MaxPriorityFeePerGas = (*big.Int)(dec.MaxPriorityFeePerGas)
} }
if dec.Nonce != nil { if dec.Nonce != nil {
s.Nonce = uint64(*dec.Nonce) s.Nonce = (*big.Int)(dec.Nonce)
} }
if dec.To != nil { if dec.To != nil {
s.To = *dec.To s.To = *dec.To

View file

@ -487,7 +487,7 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
}, },
}, },
"OsakaToBPO1AtTime15k": { "OsakaToBPO1AtTime15k": {
@ -515,7 +515,7 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
}, },
}, },
"BPO2": { "BPO2": {
@ -544,8 +544,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
}, },
}, },
"BPO1ToBPO2AtTime15k": { "BPO1ToBPO2AtTime15k": {
@ -574,8 +574,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
}, },
}, },
"BPO3": { "BPO3": {
@ -605,8 +605,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig, BPO3: params.DefaultBPO3BlobConfig,
}, },
}, },
@ -637,8 +637,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig, BPO3: params.DefaultBPO3BlobConfig,
}, },
}, },
@ -670,8 +670,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig, BPO3: params.DefaultBPO3BlobConfig,
BPO4: params.DefaultBPO4BlobConfig, BPO4: params.DefaultBPO4BlobConfig,
}, },
@ -704,8 +704,8 @@ var Forks = map[string]*params.ChainConfig{
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig, BPO3: params.DefaultBPO3BlobConfig,
BPO4: params.DefaultBPO4BlobConfig, BPO4: params.DefaultBPO4BlobConfig,
}, },
@ -732,17 +732,44 @@ var Forks = map[string]*params.ChainConfig{
OsakaTime: u64(0), OsakaTime: u64(0),
BPO1Time: u64(0), BPO1Time: u64(0),
BPO2Time: u64(0), BPO2Time: u64(0),
BPO3Time: u64(0),
BPO4Time: u64(0),
AmsterdamTime: u64(0), AmsterdamTime: u64(0),
DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, DepositContractAddress: params.MainnetChainConfig.DepositContractAddress,
BlobScheduleConfig: &params.BlobScheduleConfig{ BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig, Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig, Prague: params.DefaultPragueBlobConfig,
BPO1: bpo1BlobConfig, BPO1: params.DefaultBPO1BlobConfig,
BPO2: bpo2BlobConfig, BPO2: params.DefaultBPO2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig, },
BPO4: params.DefaultBPO4BlobConfig, },
"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: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig,
BPO1: params.DefaultBPO1BlobConfig,
BPO2: params.DefaultBPO2BlobConfig,
}, },
}, },
"Verkle": { "Verkle": {
@ -793,16 +820,11 @@ var Forks = map[string]*params.ChainConfig{
}, },
} }
var bpo1BlobConfig = &params.BlobConfig{ func init() {
Target: 9, // Execution-spec-tests fixtures use the historical upgrade names for
Max: 14, // the EIP150 and EIP158 rulesets.
UpdateFraction: 8832827, Forks["TangerineWhistle"] = Forks["EIP150"]
} Forks["SpuriousDragon"] = Forks["EIP158"]
var bpo2BlobConfig = &params.BlobConfig{
Target: 14,
Max: 21,
UpdateFraction: 13739630,
} }
// AvailableForks returns the set of defined fork names // AvailableForks returns the set of defined fork names

View file

@ -97,10 +97,8 @@ func TestExecutionSpecState(t *testing.T) {
st := new(testMatcher) st := new(testMatcher)
// Broken tests // Broken tests
st.skipLoad(`RevertInCreateInInit`) st.skipLoad(`.*eip7610_create_collision/initcollision/.*`)
st.skipLoad(`InitCollisionParis`) st.skipLoad(`.*eip7610_create_collision/revert_in_create/.*`)
st.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
st.skipLoad(`create2collisionStorageParis`)
st.walk(t, executionSpecStateTestDir, func(t *testing.T, name string, test *StateTest) { st.walk(t, executionSpecStateTestDir, func(t *testing.T, name string, test *StateTest) {
execStateTest(t, st, test) execStateTest(t, st, test)

View file

@ -95,6 +95,7 @@ type stEnv struct {
Timestamp uint64 `json:"currentTimestamp" gencodec:"required"` Timestamp uint64 `json:"currentTimestamp" gencodec:"required"`
BaseFee *big.Int `json:"currentBaseFee" gencodec:"optional"` BaseFee *big.Int `json:"currentBaseFee" gencodec:"optional"`
ExcessBlobGas *uint64 `json:"currentExcessBlobGas" gencodec:"optional"` ExcessBlobGas *uint64 `json:"currentExcessBlobGas" gencodec:"optional"`
SlotNumber *uint64 `json:"slotNumber" gencodec:"optional"`
} }
type stEnvMarshaling struct { type stEnvMarshaling struct {
@ -106,6 +107,7 @@ type stEnvMarshaling struct {
Timestamp math.HexOrDecimal64 Timestamp math.HexOrDecimal64
BaseFee *math.HexOrDecimal256 BaseFee *math.HexOrDecimal256
ExcessBlobGas *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64
SlotNumber *math.HexOrDecimal64
} }
//go:generate go run github.com/fjl/gencodec -type stTransaction -field-override stTransactionMarshaling -out gen_sttransaction.go //go:generate go run github.com/fjl/gencodec -type stTransaction -field-override stTransactionMarshaling -out gen_sttransaction.go
@ -114,7 +116,7 @@ type stTransaction struct {
GasPrice *big.Int `json:"gasPrice"` GasPrice *big.Int `json:"gasPrice"`
MaxFeePerGas *big.Int `json:"maxFeePerGas"` MaxFeePerGas *big.Int `json:"maxFeePerGas"`
MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"` MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"`
Nonce uint64 `json:"nonce"` Nonce *big.Int `json:"nonce"`
To string `json:"to"` To string `json:"to"`
Data []string `json:"data"` Data []string `json:"data"`
AccessLists []*types.AccessList `json:"accessLists,omitempty"` AccessLists []*types.AccessList `json:"accessLists,omitempty"`
@ -131,7 +133,7 @@ type stTransactionMarshaling struct {
GasPrice *math.HexOrDecimal256 GasPrice *math.HexOrDecimal256
MaxFeePerGas *math.HexOrDecimal256 MaxFeePerGas *math.HexOrDecimal256
MaxPriorityFeePerGas *math.HexOrDecimal256 MaxPriorityFeePerGas *math.HexOrDecimal256
Nonce math.HexOrDecimal64 Nonce *math.HexOrDecimal256
GasLimit []math.HexOrDecimal64 GasLimit []math.HexOrDecimal64
PrivateKey hexutil.Bytes PrivateKey hexutil.Bytes
BlobGasFeeCap *math.HexOrDecimal256 BlobGasFeeCap *math.HexOrDecimal256
@ -378,6 +380,7 @@ func (t *StateTest) genesis(config *params.ChainConfig) *core.Genesis {
GasLimit: t.json.Env.GasLimit, GasLimit: t.json.Env.GasLimit,
Number: t.json.Env.Number, Number: t.json.Env.Number,
Timestamp: t.json.Env.Timestamp, Timestamp: t.json.Env.Timestamp,
SlotNumber: t.json.Env.SlotNumber,
Alloc: t.json.Pre, Alloc: t.json.Pre,
} }
if t.json.Env.Random != nil { if t.json.Env.Random != nil {
@ -389,6 +392,16 @@ func (t *StateTest) genesis(config *params.ChainConfig) *core.Genesis {
} }
func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Message, error) { 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 var from common.Address
// If 'sender' field is present, use that // If 'sender' field is present, use that
if tx.Sender != nil { if tx.Sender != nil {
@ -478,7 +491,7 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess
msg := &core.Message{ msg := &core.Message{
From: from, From: from,
To: to, To: to,
Nonce: tx.Nonce, Nonce: nonce,
Value: uint256.MustFromBig(value), Value: uint256.MustFromBig(value),
GasLimit: gasLimit, GasLimit: gasLimit,
GasPrice: uint256.MustFromBig(gasPrice), GasPrice: uint256.MustFromBig(gasPrice),

View file

@ -70,7 +70,7 @@ func TestExecutionSpecTransaction(t *testing.T) {
st := new(testMatcher) st := new(testMatcher)
// Emptiness of authorization list is only validated during the tx precheck // 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) { st.walk(t, executionSpecTransactionTestDir, func(t *testing.T, name string, test *TransactionTest) {
if err := st.checkFailure(t, test.Run()); err != nil { if err := st.checkFailure(t, test.Run()); err != nil {

View file

@ -86,11 +86,11 @@ func (tt *TransactionTest) Run() error {
if overflow { if overflow {
return sender, hash, 0, errors.New("value exceeds 256 bits") 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 { if err != nil {
return return
} }
requiredGas = cost.RegularGas requiredGas = cost
if requiredGas > tx.Gas() { if requiredGas > tx.Gas() {
return sender, hash, 0, fmt.Errorf("insufficient gas ( %d < %d )", tx.Gas(), requiredGas) return sender, hash, 0, fmt.Errorf("insufficient gas ( %d < %d )", tx.Gas(), requiredGas)
} }
@ -125,6 +125,8 @@ func (tt *TransactionTest) Run() error {
{"Shanghai", true}, {"Shanghai", true},
{"Cancun", true}, {"Cancun", true},
{"Prague", true}, {"Prague", true},
{"Osaka", true},
{"Amsterdam", true},
} { } {
expected := tt.Result[testcase.name] expected := tt.Result[testcase.name]
if expected == nil { if expected == nil {

View file

@ -543,15 +543,15 @@ func (db *Database) Recoverable(root common.Hash) bool {
if db.stateFreezer == nil { if db.stateFreezer == nil {
return false return false
} }
// Ensure the requested state is a canonical state and all state blob := rawdb.ReadStateHistoryMeta(db.stateFreezer, *id+1)
// histories in range [id+1, dl.ID] are present and complete. if len(blob) == 0 {
return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error { return false // pruned from the tail or otherwise unavailable
if m.parent != root { }
return errors.New("unexpected state history") var m meta
} if err := m.decode(blob); err != nil {
root = m.root return false
return nil }
}) == nil return m.parent == root
} }
// Close closes the trie database and the held freezer. // Close closes the trie database and the held freezer.

View file

@ -612,30 +612,3 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
return nil 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
}