diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index efe76cf170..a3fa4a2ea7 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -145,7 +145,7 @@ jobs:
windows:
name: Windows Build
- runs-on: "win-11"
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -155,57 +155,46 @@ jobs:
go-version: 1.24
cache: false
- # Note: gcc.exe only works properly if the corresponding bin/ directory is
- # contained in PATH.
+ - name: Install cross toolchain
+ run: |
+ apt-get update
+ apt-get -yq --no-install-suggests --no-install-recommends install \
+ gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 nsis
- name: "Build (amd64)"
- shell: cmd
run: |
- set PATH=%GETH_MINGW%\bin;%PATH%
- go run build/ci.go install -dlgo -arch amd64 -cc %GETH_MINGW%\bin\gcc.exe
- env:
- GETH_MINGW: 'C:\msys64\mingw64'
+ go run build/ci.go install -dlgo -os windows -arch amd64 -cc x86_64-w64-mingw32-gcc
- name: "Create/upload archive (amd64)"
- shell: cmd
run: |
- go run build/ci.go archive -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
+ go run build/ci.go archive -os windows -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
env:
WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
- name: "Create/upload NSIS installer (amd64)"
- shell: cmd
run: |
- set "PATH=C:\Program Files (x86)\NSIS;%PATH%"
go run build/ci.go nsis -arch amd64 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
- del /Q build\bin\*
+ rm -f build/bin/*
env:
WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
- name: "Build (386)"
- shell: cmd
run: |
- set PATH=%GETH_MINGW%\bin;%PATH%
- go run build/ci.go install -dlgo -arch 386 -cc %GETH_MINGW%\bin\gcc.exe
- env:
- GETH_MINGW: 'C:\msys64\mingw32'
+ go run build/ci.go install -dlgo -os windows -arch 386 -cc i686-w64-mingw32-gcc
- name: "Create/upload archive (386)"
- shell: cmd
run: |
- go run build/ci.go archive -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
+ go run build/ci.go archive -os windows -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
env:
WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
- name: "Create/upload NSIS installer (386)"
- shell: cmd
run: |
- set "PATH=C:\Program Files (x86)\NSIS;%PATH%"
go run build/ci.go nsis -arch 386 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
- del /Q build\bin\*
+ rm -f build/bin/*
env:
WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
diff --git a/accounts/abi/bind/old.go b/accounts/abi/bind/old.go
index b09f5f3c7a..1fe1b1cca5 100644
--- a/accounts/abi/bind/old.go
+++ b/accounts/abi/bind/old.go
@@ -176,6 +176,13 @@ var (
// ErrNoCodeAfterDeploy is returned by WaitDeployed if contract creation leaves
// an empty contract behind.
ErrNoCodeAfterDeploy = bind2.ErrNoCodeAfterDeploy
+
+ // ErrNoEventSignature is returned when a log entry has no topics.
+ ErrNoEventSignature = bind2.ErrNoEventSignature
+
+ // ErrEventSignatureMismatch is returned when a log's topic[0] does not match
+ // the expected event signature.
+ ErrEventSignatureMismatch = bind2.ErrEventSignatureMismatch
)
// ContractCaller defines the methods needed to allow operating with a contract on a read
diff --git a/accounts/abi/unpack_test.go b/accounts/abi/unpack_test.go
index 90713c03ca..90cfa68655 100644
--- a/accounts/abi/unpack_test.go
+++ b/accounts/abi/unpack_test.go
@@ -910,7 +910,7 @@ func TestUnpackTuple(t *testing.T) {
},
},
FieldT: T{
- big.NewInt(0), big.NewInt(1),
+ big.NewInt(0).SetBits([]big.Word{}), big.NewInt(1),
},
A: big.NewInt(1),
}
@@ -919,7 +919,7 @@ func TestUnpackTuple(t *testing.T) {
if err != nil {
t.Error(err)
}
- if reflect.DeepEqual(ret, expected) {
+ if !reflect.DeepEqual(ret, expected) {
t.Error("unexpected unpack value")
}
}
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index aeafcfc838..0000000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-clone_depth: 5
-version: "{branch}.{build}"
-
-image:
- - Visual Studio 2019
-
-environment:
- matrix:
- - GETH_ARCH: amd64
- GETH_MINGW: 'C:\msys64\mingw64'
- - GETH_ARCH: 386
- GETH_MINGW: 'C:\msys64\mingw32'
-
-install:
- - git submodule update --init --depth 1 --recursive
- - go version
-
-for:
- # Windows builds for amd64 + 386.
- - matrix:
- only:
- - image: Visual Studio 2019
- environment:
- # We use gcc from MSYS2 because it is the most recent compiler version available on
- # AppVeyor. Note: gcc.exe only works properly if the corresponding bin/ directory is
- # contained in PATH.
- GETH_CC: '%GETH_MINGW%\bin\gcc.exe'
- PATH: '%GETH_MINGW%\bin;C:\Program Files (x86)\NSIS\;%PATH%'
- build_script:
- - 'echo %GETH_ARCH%'
- - 'echo %GETH_CC%'
- - '%GETH_CC% --version'
- - go run build/ci.go install -dlgo -arch %GETH_ARCH% -cc %GETH_CC%
- after_build:
- # Upload builds. Note that ci.go makes this a no-op PR builds.
- - go run build/ci.go archive -arch %GETH_ARCH% -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
- - go run build/ci.go nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
- test_script:
- - go run build/ci.go test -dlgo -arch %GETH_ARCH% -cc %GETH_CC% -short
diff --git a/beacon/engine/errors.go b/beacon/engine/errors.go
index 62773a0ea9..80e13b11b9 100644
--- a/beacon/engine/errors.go
+++ b/beacon/engine/errors.go
@@ -81,6 +81,7 @@ var (
TooLargeRequest = &EngineAPIError{code: -38004, msg: "Too large request"}
InvalidParams = &EngineAPIError{code: -32602, msg: "Invalid parameters"}
UnsupportedFork = &EngineAPIError{code: -38005, msg: "Unsupported fork"}
+ TooDeepReorg = &EngineAPIError{code: -38006, msg: "Too deep reorg"}
STATUS_INVALID = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: INVALID}, PayloadID: nil}
STATUS_SYNCING = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: SYNCING}, PayloadID: nil}
diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go
index c733b3f350..02a1fd3805 100644
--- a/beacon/engine/gen_ed.go
+++ b/beacon/engine/gen_ed.go
@@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
)
var _ = (*executableDataMarshaling)(nil)
@@ -17,24 +18,25 @@ var _ = (*executableDataMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (e ExecutableData) MarshalJSON() ([]byte, error) {
type ExecutableData struct {
- ParentHash common.Hash `json:"parentHash" gencodec:"required"`
- FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
- StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
- ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
- LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"`
- Random common.Hash `json:"prevRandao" gencodec:"required"`
- Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
- GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
- GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
- Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
- ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"`
- BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
- BlockHash common.Hash `json:"blockHash" gencodec:"required"`
- Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
- Withdrawals []*types.Withdrawal `json:"withdrawals"`
- BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
- ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
- SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ ParentHash common.Hash `json:"parentHash" gencodec:"required"`
+ FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
+ StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
+ ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
+ LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"`
+ Random common.Hash `json:"prevRandao" gencodec:"required"`
+ Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
+ GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
+ GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
+ Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
+ ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"`
+ BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
+ BlockHash common.Hash `json:"blockHash" gencodec:"required"`
+ Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
+ Withdrawals []*types.Withdrawal `json:"withdrawals"`
+ BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
+ ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
var enc ExecutableData
enc.ParentHash = e.ParentHash
@@ -60,30 +62,32 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed)
enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas)
enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber)
+ enc.BlockAccessList = e.BlockAccessList
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (e *ExecutableData) UnmarshalJSON(input []byte) error {
type ExecutableData struct {
- ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
- FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"`
- StateRoot *common.Hash `json:"stateRoot" gencodec:"required"`
- ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"`
- LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"`
- Random *common.Hash `json:"prevRandao" gencodec:"required"`
- Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
- GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
- GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
- Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
- ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"`
- BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
- BlockHash *common.Hash `json:"blockHash" gencodec:"required"`
- Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
- Withdrawals []*types.Withdrawal `json:"withdrawals"`
- BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
- ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
- SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
+ FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"`
+ StateRoot *common.Hash `json:"stateRoot" gencodec:"required"`
+ ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"`
+ LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"`
+ Random *common.Hash `json:"prevRandao" gencodec:"required"`
+ Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
+ GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
+ GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
+ Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
+ ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"`
+ BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
+ BlockHash *common.Hash `json:"blockHash" gencodec:"required"`
+ Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
+ Withdrawals []*types.Withdrawal `json:"withdrawals"`
+ BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
+ ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
var dec ExecutableData
if err := json.Unmarshal(input, &dec); err != nil {
@@ -160,5 +164,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
if dec.SlotNumber != nil {
e.SlotNumber = (*uint64)(dec.SlotNumber)
}
+ if dec.BlockAccessList != nil {
+ e.BlockAccessList = dec.BlockAccessList
+ }
return nil
}
diff --git a/beacon/engine/types.go b/beacon/engine/types.go
index 8824e40f5e..7cc49c637e 100644
--- a/beacon/engine/types.go
+++ b/beacon/engine/types.go
@@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
)
@@ -82,24 +83,25 @@ type payloadAttributesMarshaling struct {
// ExecutableData is the data necessary to execute an EL payload.
type ExecutableData struct {
- ParentHash common.Hash `json:"parentHash" gencodec:"required"`
- FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
- StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
- ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
- LogsBloom []byte `json:"logsBloom" gencodec:"required"`
- Random common.Hash `json:"prevRandao" gencodec:"required"`
- Number uint64 `json:"blockNumber" gencodec:"required"`
- GasLimit uint64 `json:"gasLimit" gencodec:"required"`
- GasUsed uint64 `json:"gasUsed" gencodec:"required"`
- Timestamp uint64 `json:"timestamp" gencodec:"required"`
- ExtraData []byte `json:"extraData" gencodec:"required"`
- BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
- BlockHash common.Hash `json:"blockHash" gencodec:"required"`
- Transactions [][]byte `json:"transactions" gencodec:"required"`
- Withdrawals []*types.Withdrawal `json:"withdrawals"`
- BlobGasUsed *uint64 `json:"blobGasUsed"`
- ExcessBlobGas *uint64 `json:"excessBlobGas"`
- SlotNumber *uint64 `json:"slotNumber,omitempty"`
+ ParentHash common.Hash `json:"parentHash" gencodec:"required"`
+ FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
+ StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
+ ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
+ LogsBloom []byte `json:"logsBloom" gencodec:"required"`
+ Random common.Hash `json:"prevRandao" gencodec:"required"`
+ Number uint64 `json:"blockNumber" gencodec:"required"`
+ GasLimit uint64 `json:"gasLimit" gencodec:"required"`
+ GasUsed uint64 `json:"gasUsed" gencodec:"required"`
+ Timestamp uint64 `json:"timestamp" gencodec:"required"`
+ ExtraData []byte `json:"extraData" gencodec:"required"`
+ BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
+ BlockHash common.Hash `json:"blockHash" gencodec:"required"`
+ Transactions [][]byte `json:"transactions" gencodec:"required"`
+ Withdrawals []*types.Withdrawal `json:"withdrawals"`
+ BlobGasUsed *uint64 `json:"blobGasUsed"`
+ ExcessBlobGas *uint64 `json:"excessBlobGas"`
+ SlotNumber *uint64 `json:"slotNumber,omitempty"`
+ BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
}
// JSON type overrides for executableData.
@@ -284,7 +286,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
if data.BaseFeePerGas != nil && (data.BaseFeePerGas.Sign() == -1 || data.BaseFeePerGas.BitLen() > 256) {
return nil, fmt.Errorf("invalid baseFeePerGas: %v", data.BaseFeePerGas)
}
- var blobHashes = make([]common.Hash, 0, len(txs))
+ var blobHashes = make([]common.Hash, 0, len(versionedHashes))
for _, tx := range txs {
blobHashes = append(blobHashes, tx.BlobHashes()...)
}
@@ -311,56 +313,66 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
requestsHash = &h
}
- header := &types.Header{
- ParentHash: data.ParentHash,
- UncleHash: types.EmptyUncleHash,
- Coinbase: data.FeeRecipient,
- Root: data.StateRoot,
- TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
- ReceiptHash: data.ReceiptsRoot,
- Bloom: types.BytesToBloom(data.LogsBloom),
- Difficulty: common.Big0,
- Number: new(big.Int).SetUint64(data.Number),
- GasLimit: data.GasLimit,
- GasUsed: data.GasUsed,
- Time: data.Timestamp,
- BaseFee: data.BaseFeePerGas,
- Extra: data.ExtraData,
- MixDigest: data.Random,
- WithdrawalsHash: withdrawalsRoot,
- ExcessBlobGas: data.ExcessBlobGas,
- BlobGasUsed: data.BlobGasUsed,
- ParentBeaconRoot: beaconRoot,
- RequestsHash: requestsHash,
- SlotNumber: data.SlotNumber,
+ // If Amsterdam is enabled, data.BlockAccessList is always non-nil,
+ // even for empty blocks with no state transitions.
+ //
+ // If Amsterdam is not enabled yet, blockAccessListHash is expected
+ // to be nil.
+ var blockAccessListHash *common.Hash
+ if data.BlockAccessList != nil {
+ hash := data.BlockAccessList.Hash()
+ blockAccessListHash = &hash
}
- return types.NewBlockWithHeader(header).
- WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}),
- nil
+ header := &types.Header{
+ ParentHash: data.ParentHash,
+ UncleHash: types.EmptyUncleHash,
+ Coinbase: data.FeeRecipient,
+ Root: data.StateRoot,
+ TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
+ ReceiptHash: data.ReceiptsRoot,
+ Bloom: types.BytesToBloom(data.LogsBloom),
+ Difficulty: common.Big0,
+ Number: new(big.Int).SetUint64(data.Number),
+ GasLimit: data.GasLimit,
+ GasUsed: data.GasUsed,
+ Time: data.Timestamp,
+ BaseFee: data.BaseFeePerGas,
+ Extra: data.ExtraData,
+ MixDigest: data.Random,
+ WithdrawalsHash: withdrawalsRoot,
+ ExcessBlobGas: data.ExcessBlobGas,
+ BlobGasUsed: data.BlobGasUsed,
+ ParentBeaconRoot: beaconRoot,
+ RequestsHash: requestsHash,
+ SlotNumber: data.SlotNumber,
+ BlockAccessListHash: blockAccessListHash,
+ }
+ return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil
}
// BlockToExecutableData constructs the ExecutableData structure by filling the
// fields from the given block. It assumes the given block is post-merge block.
func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope {
data := &ExecutableData{
- BlockHash: block.Hash(),
- ParentHash: block.ParentHash(),
- FeeRecipient: block.Coinbase(),
- StateRoot: block.Root(),
- Number: block.NumberU64(),
- GasLimit: block.GasLimit(),
- GasUsed: block.GasUsed(),
- BaseFeePerGas: block.BaseFee(),
- Timestamp: block.Time(),
- ReceiptsRoot: block.ReceiptHash(),
- LogsBloom: block.Bloom().Bytes(),
- Transactions: encodeTransactions(block.Transactions()),
- Random: block.MixDigest(),
- ExtraData: block.Extra(),
- Withdrawals: block.Withdrawals(),
- BlobGasUsed: block.BlobGasUsed(),
- ExcessBlobGas: block.ExcessBlobGas(),
- SlotNumber: block.SlotNumber(),
+ BlockHash: block.Hash(),
+ ParentHash: block.ParentHash(),
+ FeeRecipient: block.Coinbase(),
+ StateRoot: block.Root(),
+ Number: block.NumberU64(),
+ GasLimit: block.GasLimit(),
+ GasUsed: block.GasUsed(),
+ BaseFeePerGas: block.BaseFee(),
+ Timestamp: block.Time(),
+ ReceiptsRoot: block.ReceiptHash(),
+ LogsBloom: block.Bloom().Bytes(),
+ Transactions: encodeTransactions(block.Transactions()),
+ Random: block.MixDigest(),
+ ExtraData: block.Extra(),
+ Withdrawals: block.Withdrawals(),
+ BlobGasUsed: block.BlobGasUsed(),
+ ExcessBlobGas: block.ExcessBlobGas(),
+ SlotNumber: block.SlotNumber(),
+ BlockAccessList: block.AccessList(),
}
// Add blobs.
diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go
index 4fa87785c0..7fc735d893 100644
--- a/beacon/light/committee_chain.go
+++ b/beacon/light/committee_chain.go
@@ -182,6 +182,12 @@ func (s *CommitteeChain) Reset() {
s.chainmu.Lock()
defer s.chainmu.Unlock()
+ s.resetLocked()
+}
+
+// ResetLocked resets the committee chain without locking. The caller should hold
+// the chainmu lock.
+func (s *CommitteeChain) resetLocked() {
if err := s.rollback(0); err != nil {
log.Error("Error writing batch into chain database", "error", err)
}
@@ -201,22 +207,22 @@ func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error {
}
period := bootstrap.Header.SyncPeriod()
if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil {
- s.Reset()
+ s.resetLocked()
return err
}
if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil {
- s.Reset()
+ s.resetLocked()
if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil {
- s.Reset()
+ s.resetLocked()
return err
}
}
if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil {
- s.Reset()
+ s.resetLocked()
return err
}
if err := s.addCommittee(period, bootstrap.Committee); err != nil {
- s.Reset()
+ s.resetLocked()
return err
}
s.changeCounter++
diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go
index d84a3d64da..b15b967433 100644
--- a/beacon/light/sync/update_sync.go
+++ b/beacon/light/sync/update_sync.go
@@ -98,7 +98,10 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E
case ssDefault:
if resp != nil {
if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) {
- s.chain.CheckpointInit(*checkpoint)
+ err := s.chain.CheckpointInit(*checkpoint)
+ if err != nil {
+ return
+ }
s.initialized = true
return
}
diff --git a/build/checksums.txt b/build/checksums.txt
index 1832ce41dd..454efa93c4 100644
--- a/build/checksums.txt
+++ b/build/checksums.txt
@@ -5,49 +5,49 @@
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz
-# version:golang 1.25.9
+# version:golang 1.25.10
# https://go.dev/dl/
-0ec9ef8ebcea097aac37decae9f09a7218b451cd96be7d6ed513d8e4bcf909cf go1.25.9.src.tar.gz
-b9ede6378a8f8d3d22bf52e68beb69ef7abdb65929ab2456020383002da15846 go1.25.9.aix-ppc64.tar.gz
-92cb78fba4796e218c1accb0ea0a214ef2094c382049a244ad6505505d015fbe go1.25.9.darwin-amd64.tar.gz
-9528be7329b9770631a6bd09ca2f3a73ed7332bec01d87435e75e92d8f130363 go1.25.9.darwin-arm64.tar.gz
-918e44a471c5524caa52f74185064240d5eb343aa8023d604776511fc7adffa6 go1.25.9.dragonfly-amd64.tar.gz
-2d67dbdfd09c6fcaa0e64485367ef43b8837ea200c663d6417183237bcddf83d go1.25.9.freebsd-386.tar.gz
-9152d0c0badbfeb0c0e148e47c12bec28099d8cf2db60958810c879e0b679d07 go1.25.9.freebsd-amd64.tar.gz
-437dca59604ad4a806a6a88e3d7ec1cd98ac9b402a3671629f4e553dd8b9888f go1.25.9.freebsd-arm.tar.gz
-4c0fe53977412036fc8081e8d0992bbaabe4d3e1926137271ba11c2f5753300f go1.25.9.freebsd-arm64.tar.gz
-d6087cdd1c084bd186132f29e0d032852a745f3c7619003d0fd5612c1fa58c8a go1.25.9.freebsd-riscv64.tar.gz
-f82e49037e195cb62beae6a6ad83497157b2af5a01bad2f1dcb65df41080aabb go1.25.9.illumos-amd64.tar.gz
-1e14a73bc2b19e370e0d4c57ba87aabfe8aef1e435e14d246742d48a13254f36 go1.25.9.linux-386.tar.gz
-00859d7bd6defe8bf84d9db9e57b9a4467b2887c18cd93ae7460e713db774bc1 go1.25.9.linux-amd64.tar.gz
-ec342e7389b7f489564ed5463c63b16cf8040023dabc7861256677165a8c0e2b go1.25.9.linux-arm64.tar.gz
-7d4f0d266d871301e08ef4ac31c56e66048688893b2848392e5c600276351ee8 go1.25.9.linux-armv6l.tar.gz
-f3460d901a14496bc609636e4accf9110ee1869d41c64af7e29cd567cffcf49b go1.25.9.linux-loong64.tar.gz
-1da96ea449382ff96c09c55cee74815324e01d687d5ac6d2ade58244b8574306 go1.25.9.linux-mips.tar.gz
-311a7f5f01f9a4bd51288b575eb619dc8e28e1fbc0cd78256a428b3ca668ff01 go1.25.9.linux-mips64.tar.gz
-0b4edaf9e2ba3f0a079547effda70ec6a4b51a6ca3271a1147652c87ebcf3735 go1.25.9.linux-mips64le.tar.gz
-42667340df264896f20b12261429d954e736e9772ab83ba289e68c30cf6f9628 go1.25.9.linux-mipsle.tar.gz
-b9cbb3a4894b5aca6966c23452608435e8535278ef019b18d8898fbbfab67e74 go1.25.9.linux-ppc64.tar.gz
-b0c41c7da1fc8d39020d65296a0dc54167afd9f76d67064e22c31ce3d839a739 go1.25.9.linux-ppc64le.tar.gz
-2a630be8f854177c13e5fa75f7812c721369ecb9bd6e4c0fb1bd1c708d08b37c go1.25.9.linux-riscv64.tar.gz
-0cf55136ac7eaccfc36d849054f849510ea289c2d959ffbed7b3866b4f484d17 go1.25.9.linux-s390x.tar.gz
-eaf8167ff10a6a3e5dd304ef5f2e020b3a7379e76fa1011dc49c895800bf367c go1.25.9.netbsd-386.tar.gz
-3cc6a861e62e23feae660984e0f2f14a2efb5d1f655900afee1d51af98919ae4 go1.25.9.netbsd-amd64.tar.gz
-c2c44dca10e882c30553f4aa2ab8f6722b670fb12882378c8f461a9105d40188 go1.25.9.netbsd-arm.tar.gz
-f301b71a8ec448053a5d2597df2e178120204bc9a33266c81600dd5d020a61b4 go1.25.9.netbsd-arm64.tar.gz
-c4543b7fdef9707b4896810c69b4160a43ecec210af45c300f3abd78aa0c9e72 go1.25.9.openbsd-386.tar.gz
-37275325e314f5ab7cf8ae65c4efc7cbfdaf20b41c6849549739b57a3ac97544 go1.25.9.openbsd-amd64.tar.gz
-f9c05b6b315e979ecdd47354dd287c01708d6a88dc6ae7af74c84df8fa00df94 go1.25.9.openbsd-arm.tar.gz
-4e999f42cf959ff95ca84af1ea1db3771000f5e57e157904bc2ffc72c75e29a2 go1.25.9.openbsd-arm64.tar.gz
-0c7fa6c7c2b1cc13ad32fa94fc31273b4adf39c1e0f0e5dcedac158ff526af3f go1.25.9.openbsd-ppc64.tar.gz
-347b33953a4b6e8df17719296f360f60878fe48a2d482ceb3637a3dfd4950065 go1.25.9.openbsd-riscv64.tar.gz
-889f77d567c06832e0d332fe2458653dc66d43cded7ddbca6f72ce0ca60029cc go1.25.9.plan9-386.tar.gz
-978b1f931fadec2f2516237d2649ee845d93c8eaf47dd196cfd8d26c7b2706a1 go1.25.9.plan9-amd64.tar.gz
-30b9565e5ad0a212fe00990ead700c751b416eb2ef8d7c91a204945a7ff83a48 go1.25.9.plan9-arm.tar.gz
-9e9125ff84ab3c3522ec758cab9540a17e9cba12bfcc34b6bf556cb89b522591 go1.25.9.solaris-amd64.tar.gz
-bf40515f5f4d834fa9ead31ff75581e61a38ac27bf49840b95c5c998d321c0f6 go1.25.9.windows-386.zip
-a7a710e225467b34e9e09fb432b829c86c9b2da5821ee5418f7eb2e8ae1a22cc go1.25.9.windows-amd64.zip
-33cd73cf1b3ceee655ef71bc96e94006c02ae3c617fdd67ac9be3dfae3957449 go1.25.9.windows-arm64.zip
+20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz
+a194e767c2ab4216a60acc068b9dbe6bf4fae05c14bb52d6bbdcb5b3ea521308 go1.25.10.aix-ppc64.tar.gz
+52321165a3146cd91865ef98371506a846ed4dc4f9f1c9323e5ad90d2a411e06 go1.25.10.darwin-amd64.tar.gz
+795691a425de7e7cdba3544f354dcd2cebcf52e87dc6898193878f34eb6d634f go1.25.10.darwin-arm64.tar.gz
+e37b4544ba9e9e9a7ab2ed3116b3fc4d39a88da854baa5a566d9d6d3a9de7d4c go1.25.10.dragonfly-amd64.tar.gz
+2a70d1fdabab637aa442ca94599a56e381238efa20cb995d5433b8579bfe482c go1.25.10.freebsd-386.tar.gz
+9cdf522d87d47d82fec4a313cc4f8c3c94a7770426e8d443e4150a1f330cba71 go1.25.10.freebsd-amd64.tar.gz
+6da6183633e9e59ffd9edefab68b5059c89b605596d94aaba650b1681fccd35f go1.25.10.freebsd-arm.tar.gz
+7adcefeebdd05331f4d45f1ad2dddb5c53537cff6552e82f6595b3b833b95371 go1.25.10.freebsd-arm64.tar.gz
+285f80a1ace21a7d94035cd753196eeada8cacd48e6396fd116ad5eb67aea957 go1.25.10.freebsd-riscv64.tar.gz
+de7461bf0e5068a4f6e7f8713026d70516be6dbd5de5d21f9ced1c182f2f326e go1.25.10.illumos-amd64.tar.gz
+2f574f2e2e19ead5b280fec0e7af5c81b76632685f03b6ac42dfa34c4b773c52 go1.25.10.linux-386.tar.gz
+42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70 go1.25.10.linux-amd64.tar.gz
+654da1f9b50a5d1c2a85ccf8ed405aa89c06e94d18384628bf186f7712677b08 go1.25.10.linux-arm64.tar.gz
+39f168f158e693887d3ad006168af1b1a3007b19c5993cae4d9d57f82f52aaf8 go1.25.10.linux-armv6l.tar.gz
+05401fe5ea50ad2bafb9c797ef9bf21574b0661f19ef4d0dd66af8a0fb7323f3 go1.25.10.linux-loong64.tar.gz
+d5bc2d6155d394a3aae41f21eb7c60da5595a6147aa0f30ed6b27da25e06c3f7 go1.25.10.linux-mips.tar.gz
+8c64e7493e5953c3ba3153487d2fddd7f8ed142392c77f138e6792a6c1930db4 go1.25.10.linux-mips64.tar.gz
+bd53aa2d558b7c1eadfc6bf01132e1859203a92f458ed7ba75b7f3230f14b095 go1.25.10.linux-mips64le.tar.gz
+120b254e2e2980bb06687175db5c4064a85696c53001dc9f59934ad18f74a6bc go1.25.10.linux-mipsle.tar.gz
+8a6acb21295b0ec974a44608361920ea8dbff5666631a6f556bd7d5f1d56535f go1.25.10.linux-ppc64.tar.gz
+778925fdcdf9a272f823d147fad51545c3334b7ccd8652b2ccaaf2b01800280a go1.25.10.linux-ppc64le.tar.gz
+b4f04ad0db48bcfea946db5323919cd21034e0bd2821a557dacd29c1b1013a4b go1.25.10.linux-riscv64.tar.gz
+936b953e43921a64c12da871f76871ebbeb6d2092a7b8bdc307f5246f3c662cc go1.25.10.linux-s390x.tar.gz
+061470e0bc7132146a5925a3cc28d5bc498eb1b1ff09dedcfaae10f781ff2274 go1.25.10.netbsd-386.tar.gz
+63b2d50d7f8f269a9c82d42a4060e90cffb7f9102299818bb071b067aac8da8f go1.25.10.netbsd-amd64.tar.gz
+c35129f68796526aa4dc4b6f481e2d995ef312aedadc88b659b945cc00e1f8f0 go1.25.10.netbsd-arm.tar.gz
+2f541da4e2b298154d992d1f11bbb38c89d0821d91cc50a46776d42bb5e63bca go1.25.10.netbsd-arm64.tar.gz
+2d42e569b07f1b99fdbfd008e7c22f967d165e2ce02464f46818fbed2aec43f5 go1.25.10.openbsd-386.tar.gz
+0ad05960e8c9f867328151308c87f938433bec8f22f6a9437a896e22169fc840 go1.25.10.openbsd-amd64.tar.gz
+099cc11473f99461c77161912740945308f08f6834980afb262c72bdc915f2d7 go1.25.10.openbsd-arm.tar.gz
+bdf3335d5008c1ddc81fa94892283e4f1fee22566f5351d4e726d9f55a67c838 go1.25.10.openbsd-arm64.tar.gz
+0933d418da0a61e0f29de717a77498f16b9b5b50dbe2205e20b2ed7fd4067f75 go1.25.10.openbsd-ppc64.tar.gz
+191e6f3e75712f8c13d189d53b668e2cac6449f26474c1d86fbd04f6e9846f9c go1.25.10.openbsd-riscv64.tar.gz
+68c053c8acd76c50fc430e92f4a86110ec3d97dd03d27b9339b4eaf793caff5f go1.25.10.plan9-386.tar.gz
+42e2c46638ae22d93402e79efb40faee5c42cf7c56a01bb3ab47c6bb2512b745 go1.25.10.plan9-amd64.tar.gz
+3ef1d5838b1648da16724a07b72e839ccbd7cb8899c3e0426afd6b79d494b91c go1.25.10.plan9-arm.tar.gz
+631e3716017fbec06500a628d97e1155daec3593f0a7812c2ebfe8fc8c96b2ab go1.25.10.solaris-amd64.tar.gz
+ddc693d2d9d7cc671ebb72d1d50aa05670f95b059b7d90440611af57976871d5 go1.25.10.windows-386.zip
+ca37af2dadd8544464f1a9ca7c3886499d1cdfcb263855d0a1d71f194b2bd222 go1.25.10.windows-amd64.zip
+38be57e0398bd93673d65bcae6dc7ee3cf151d7038d0dba5c60a5153022872da go1.25.10.windows-arm64.zip
# version:golangci 2.10.1
# https://github.com/golangci/golangci-lint/releases/
diff --git a/build/ci.go b/build/ci.go
index 173288bcdc..173a3280ce 100644
--- a/build/ci.go
+++ b/build/ci.go
@@ -73,21 +73,9 @@ var (
"./cmd/keeper",
}
- // Files that end up in the geth*.zip archive.
- gethArchiveFiles = []string{
- "COPYING",
- executablePath("geth"),
- }
-
- // Files that end up in the geth-alltools*.zip archive.
- allToolsArchiveFiles = []string{
- "COPYING",
- executablePath("abigen"),
- executablePath("evm"),
- executablePath("geth"),
- executablePath("rlpdump"),
- executablePath("clef"),
- }
+ // Files that end up in the geth-alltools*.zip archive (and the NSIS installer
+ // dev-tools section). Order matches the historical layout produced by ci.go.
+ allToolsBinaries = []string{"abigen", "evm", "geth", "rlpdump", "clef"}
// Keeper build targets with their configurations
keeperTargets = []struct {
@@ -180,13 +168,35 @@ var (
var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin"))
-func executablePath(name string) string {
- if runtime.GOOS == "windows" {
+// executablePath returns the path to a built binary in GOBIN, applying the
+// platform-specific extension for the given target OS.
+func executablePath(name, targetOS string) string {
+ if targetOS == "windows" {
name += ".exe"
}
return filepath.Join(GOBIN, name)
}
+// gethArchiveFiles returns the file list for the geth-{platform}-{ver}.zip
+// archive, with binary paths resolved for the target OS.
+func gethArchiveFiles(targetOS string) []string {
+ return []string{
+ "COPYING",
+ executablePath("geth", targetOS),
+ }
+}
+
+// allToolsArchiveFiles returns the file list for the
+// geth-alltools-{platform}-{ver}.zip archive, with binary paths resolved for
+// the target OS.
+func allToolsArchiveFiles(targetOS string) []string {
+ files := []string{"COPYING"}
+ for _, name := range allToolsBinaries {
+ files = append(files, executablePath(name, targetOS))
+ }
+ return files
+}
+
func main() {
log.SetFlags(log.Lshortfile)
@@ -233,6 +243,7 @@ func main() {
func doInstall(cmdline []string) {
var (
dlgo = flag.Bool("dlgo", false, "Download Go and build with it")
+ targetOS = flag.String("os", runtime.GOOS, "Target OS to cross build for")
arch = flag.String("arch", "", "Architecture to cross build for")
cc = flag.String("cc", "", "C compiler to cross build with")
staticlink = flag.Bool("static", false, "Create statically-linked executable")
@@ -241,7 +252,7 @@ func doInstall(cmdline []string) {
env := build.Env()
// Configure the toolchain.
- tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
+ tc := build.GoToolchain{GOOS: *targetOS, GOARCH: *arch, CC: *cc}
if *dlgo {
csdb := download.MustLoadChecksums("build/checksums.txt")
tc.Root = build.DownloadGo(csdb)
@@ -255,7 +266,7 @@ func doInstall(cmdline []string) {
}
// Configure the build.
- gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags)...)
+ gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags, *targetOS)...)
// Show packages during build.
gobuild.Args = append(gobuild.Args, "-v")
@@ -270,7 +281,7 @@ func doInstall(cmdline []string) {
// Do the build!
for _, pkg := range packages {
args := slices.Clone(gobuild.Args)
- args = append(args, "-o", executablePath(path.Base(pkg)))
+ args = append(args, "-o", executablePath(path.Base(pkg), *targetOS))
args = append(args, pkg)
build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env})
}
@@ -297,7 +308,13 @@ func doInstallKeeper(cmdline []string) {
tc.GOARCH = target.GOARCH
tc.GOOS = target.GOOS
tc.CC = target.CC
- gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags})...)
+ // An empty GOOS means "build for the host OS"; thread that through to
+ // buildFlags so platform-specific linker flags are picked correctly.
+ targetOS := target.GOOS
+ if targetOS == "" {
+ targetOS = runtime.GOOS
+ }
+ gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags}, targetOS)...)
gobuild.Dir = "./cmd/keeper"
gobuild.Args = append(gobuild.Args, "-v")
@@ -307,14 +324,15 @@ func doInstallKeeper(cmdline []string) {
outputName := fmt.Sprintf("keeper-%s", target.Name)
args := slices.Clone(gobuild.Args)
- args = append(args, "-o", executablePath(outputName))
+ args = append(args, "-o", executablePath(outputName, targetOS))
args = append(args, ".")
build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env, Dir: gobuild.Dir})
}
}
-// buildFlags returns the go tool flags for building.
-func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (flags []string) {
+// buildFlags returns the go tool flags for building. targetOS is the OS we
+// are producing binaries for.
+func buildFlags(env build.Environment, staticLinking bool, buildTags []string, targetOS string) (flags []string) {
var ld []string
// See https://github.com/golang/go/issues/33772#issuecomment-528176001
// We need to set --buildid to the linker here, and also pass --build-id to the
@@ -326,10 +344,10 @@ func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (
}
// Strip DWARF on darwin. This used to be required for certain things,
// and there is no downside to this, so we just keep doing it.
- if runtime.GOOS == "darwin" {
+ if targetOS == "darwin" {
ld = append(ld, "-s")
}
- if runtime.GOOS == "linux" {
+ if targetOS == "linux" {
// Enforce the stacksize to 8M, which is the case on most platforms apart from
// alpine Linux.
// See https://sourceware.org/binutils/docs-2.23.1/ld/Options.html#Options
@@ -682,12 +700,13 @@ func downloadProtoc(cachedir string) string {
// Release Packaging
func doArchive(cmdline []string) {
var (
- arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging")
- atype = flag.String("type", "zip", "Type of archive to write (zip|tar)")
- signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`)
- signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`)
- upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
- ext string
+ targetOS = flag.String("os", runtime.GOOS, "Target OS the binaries were built for")
+ arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging")
+ atype = flag.String("type", "zip", "Type of archive to write (zip|tar)")
+ signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`)
+ signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`)
+ upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
+ ext string
)
flag.CommandLine.Parse(cmdline)
switch *atype {
@@ -701,15 +720,15 @@ func doArchive(cmdline []string) {
var (
env = build.Env()
- basegeth = archiveBasename(*arch, version.Archive(env.Commit))
+ basegeth = archiveBasename(*targetOS, *arch, version.Archive(env.Commit))
geth = "geth-" + basegeth + ext
alltools = "geth-alltools-" + basegeth + ext
)
maybeSkipArchive(env)
- if err := build.WriteArchive(geth, gethArchiveFiles); err != nil {
+ if err := build.WriteArchive(geth, gethArchiveFiles(*targetOS)); err != nil {
log.Fatal(err)
}
- if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil {
+ if err := build.WriteArchive(alltools, allToolsArchiveFiles(*targetOS)); err != nil {
log.Fatal(err)
}
for _, archive := range []string{geth, alltools} {
@@ -735,7 +754,11 @@ func doKeeperArchive(cmdline []string) {
maybeSkipArchive(env)
files := []string{"COPYING"}
for _, target := range keeperTargets {
- files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name)))
+ targetOS := target.GOOS
+ if targetOS == "" {
+ targetOS = runtime.GOOS
+ }
+ files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name), targetOS))
}
if err := build.WriteArchive(keeper, files); err != nil {
log.Fatal(err)
@@ -745,8 +768,8 @@ func doKeeperArchive(cmdline []string) {
}
}
-func archiveBasename(arch string, archiveVersion string) string {
- platform := runtime.GOOS + "-" + arch
+func archiveBasename(targetOS, arch, archiveVersion string) string {
+ platform := targetOS + "-" + arch
if arch == "arm" {
platform += os.Getenv("GOARM")
}
@@ -1209,13 +1232,13 @@ func doWindowsInstaller(cmdline []string) {
env := build.Env()
maybeSkipArchive(env)
- // Aggregate binaries that are included in the installer
+ // Aggregate binaries that are included in the installer.
var (
devTools []string
allTools []string
gethTool string
)
- for _, file := range allToolsArchiveFiles {
+ for _, file := range allToolsArchiveFiles("windows") {
if file == "COPYING" { // license, copied later
continue
}
@@ -1252,16 +1275,24 @@ func doWindowsInstaller(cmdline []string) {
if env.Commit != "" {
ver[2] += "-" + env.Commit[:8]
}
- installer, err := filepath.Abs("geth-" + archiveBasename(*arch, version.Archive(env.Commit)) + ".exe")
+ installer, err := filepath.Abs("geth-" + archiveBasename("windows", *arch, version.Archive(env.Commit)) + ".exe")
if err != nil {
log.Fatalf("Failed to convert installer file path: %v", err)
}
- build.MustRunCommand("makensis.exe",
- "/DOUTPUTFILE="+installer,
- "/DMAJORVERSION="+ver[0],
- "/DMINORVERSION="+ver[1],
- "/DBUILDVERSION="+ver[2],
- "/DARCH="+*arch,
+ // makensis on Windows is "makensis.exe" with /D-style defines; on Linux
+ // (and other Unixes) the binary is "makensis" and accepts -D.
+ makensisCmd := "makensis"
+ defineFlag := "-D"
+ if runtime.GOOS == "windows" {
+ makensisCmd = "makensis.exe"
+ defineFlag = "/D"
+ }
+ build.MustRunCommand(makensisCmd,
+ defineFlag+"OUTPUTFILE="+installer,
+ defineFlag+"MAJORVERSION="+ver[0],
+ defineFlag+"MINORVERSION="+ver[1],
+ defineFlag+"BUILDVERSION="+ver[2],
+ defineFlag+"ARCH="+*arch,
filepath.Join(*workdir, "geth.nsi"),
)
// Sign and publish installer.
diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go
index c82358be49..d9d1fa02ac 100644
--- a/cmd/abigen/main.go
+++ b/cmd/abigen/main.go
@@ -215,7 +215,7 @@ func generate(c *cli.Context) error {
code string
err error
)
- if c.IsSet(v2Flag.Name) {
+ if c.Bool(v2Flag.Name) {
code, err = abigen.BindV2(types, abis, bins, c.String(pkgFlag.Name), libs, aliases)
} else {
code, err = abigen.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), libs, aliases)
diff --git a/cmd/devp2p/enrcmd.go b/cmd/devp2p/enrcmd.go
index c9b692612f..af2cf90a81 100644
--- a/cmd/devp2p/enrcmd.go
+++ b/cmd/devp2p/enrcmd.go
@@ -194,7 +194,7 @@ func formatAttrString(v rlp.RawValue) (string, bool) {
func formatAttrIP(v rlp.RawValue) (string, bool) {
content, _, err := rlp.SplitString(v)
- if err != nil || len(content) != 4 && len(content) != 6 {
+ if err != nil || len(content) != 4 && len(content) != 16 {
return "", false
}
return net.IP(content).String(), true
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 253ebe1111..043e675494 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -17,6 +17,7 @@
package t8ntool
import (
+ "context"
"encoding/json"
"fmt"
stdmath "math"
@@ -34,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/ethdb"
@@ -171,6 +173,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
includedTxs types.Transactions
blobGasUsed = uint64(0)
receipts = make(types.Receipts, 0)
+
+ // TODO return blockAccessList as a part of result
+ blockAccessList = bal.NewConstructionBlockAccessList()
)
vmContext := vm.BlockContext{
CanTransfer: core.CanTransfer,
@@ -230,14 +235,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig)
if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
+ core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList)
}
if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) {
var (
prevNumber = pre.Env.Number - 1
prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)]
)
- core.ProcessParentBlockHash(prevHash, evm)
+ core.ProcessParentBlockHash(prevHash, evm, blockAccessList)
}
for i := 0; txIt.Next(); i++ {
tx, err := txIt.Tx()
@@ -269,12 +274,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
continue
}
}
- statedb.SetTxContext(tx.Hash(), len(receipts))
+ statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1))
+
var (
snapshot = statedb.Snapshot()
gp = gaspool.Snapshot()
)
- receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm)
+ receipt, bal, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm)
if err != nil {
statedb.RevertToSnapshot(snapshot)
log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err)
@@ -291,6 +297,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
blobGasUsed += txBlobGas
receipts = append(receipts, receipt)
+ blockAccessList.Merge(bal)
}
statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber))
@@ -331,26 +338,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
// Gather the execution-layer triggered requests.
- var requests [][]byte
- if chainConfig.IsPrague(vmContext.BlockNumber, vmContext.Time) {
- requests = [][]byte{}
- // EIP-6110
- var allLogs []*types.Log
- for _, receipt := range receipts {
- allLogs = append(allLogs, receipt.Logs...)
- }
- if err := core.ParseDepositLogs(&requests, allLogs, chainConfig); err != nil {
- return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err))
- }
- // EIP-7002
- if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil {
- return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process withdrawal requests: %v", err))
- }
- // EIP-7251
- if err := core.ProcessConsolidationQueue(&requests, evm); err != nil {
- return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process consolidation requests: %v", err))
- }
+ var allLogs []*types.Log
+ for _, receipt := range receipts {
+ allLogs = append(allLogs, receipt.Logs...)
}
+ requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
+ if err != nil {
+ return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
+ }
+ blockAccessList.Merge(bal)
// Commit block
root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time))
diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go
index 7e068c06af..ca19ae3386 100644
--- a/cmd/evm/internal/t8ntool/transaction.go
+++ b/cmd/evm/internal/t8ntool/transaction.go
@@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error {
}
// Check intrinsic gas
rules := chainConfig.Rules(common.Big0, true, 0)
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
if err != nil {
r.Error = err
results = append(results, r)
@@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error {
}
// For Prague txs, validate the floor data gas.
if rules.IsPrague {
- floorDataGas, err := core.FloorDataGas(rules, tx.Data())
+ floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
r.Error = err
results = append(results, r)
diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index e0bb3a449d..89b703d3b8 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -546,7 +546,7 @@ func BinKeys(ctx *cli.Context) error {
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults)
defer db.Close()
- bt, err := genBinTrieFromAlloc(alloc, db)
+ bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth)
if err != nil {
return fmt.Errorf("error generating bt: %w", err)
}
@@ -590,7 +590,7 @@ func BinTrieRoot(ctx *cli.Context) error {
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults)
defer db.Close()
- bt, err := genBinTrieFromAlloc(alloc, db)
+ bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth)
if err != nil {
return fmt.Errorf("error generating bt: %w", err)
}
@@ -600,8 +600,8 @@ func BinTrieRoot(ctx *cli.Context) error {
}
// TODO(@CPerezz): Should this go to `bintrie` module?
-func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase) (*bintrie.BinaryTrie, error) {
- bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db)
+func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase, groupDepth int) (*bintrie.BinaryTrie, error) {
+ bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db, groupDepth)
if err != nil {
return nil, err
}
diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go
index 82e7bdff3d..6d80056d04 100644
--- a/cmd/evm/runner.go
+++ b/cmd/evm/runner.go
@@ -321,7 +321,7 @@ func runCmd(ctx *cli.Context) error {
// don't mutate the state!
runtimeConfig.State = prestate.Copy()
output, _, gasLeft, err := runtime.Create(input, &runtimeConfig)
- return output, gasLeft, err
+ return output, initialGas - gasLeft, err
}
} else {
if len(code) > 0 {
diff --git a/cmd/geth/bintrie_convert.go b/cmd/geth/bintrie_convert.go
index 43d2e629ac..46cb3aa7e4 100644
--- a/cmd/geth/bintrie_convert.go
+++ b/cmd/geth/bintrie_convert.go
@@ -151,7 +151,7 @@ func convertToBinaryTrie(ctx *cli.Context) error {
})
defer destTriedb.Close()
- binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
+ binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, ctx.Int(utils.BinTrieGroupDepthFlag.Name))
if err != nil {
return fmt.Errorf("failed to create binary trie: %w", err)
}
@@ -319,7 +319,7 @@ func commitBinaryTrie(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *t
runtime.GC()
debug.FreeOSMemory()
- bt, err := bintrie.NewBinaryTrie(newRoot, destDB)
+ bt, err := bintrie.NewBinaryTrie(newRoot, destDB, bt.GroupDepth())
if err != nil {
return nil, common.Hash{}, fmt.Errorf("failed to reload binary trie: %w", err)
}
diff --git a/cmd/geth/bintrie_convert_test.go b/cmd/geth/bintrie_convert_test.go
index 50ae752358..32e8c7e55b 100644
--- a/cmd/geth/bintrie_convert_test.go
+++ b/cmd/geth/bintrie_convert_test.go
@@ -87,7 +87,7 @@ func TestBintrieConvert(t *testing.T) {
})
defer destTriedb.Close()
- bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
+ bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8)
if err != nil {
t.Fatalf("failed to create binary trie: %v", err)
}
@@ -98,7 +98,7 @@ func TestBintrieConvert(t *testing.T) {
}
t.Logf("Binary trie root: %x", currentRoot)
- bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb)
+ bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb, 8)
if err != nil {
t.Fatalf("failed to reload binary trie: %v", err)
}
@@ -194,7 +194,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) {
PathDB: pathdb.Defaults,
})
- bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
+ bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8)
if err != nil {
t.Fatalf("failed to create binary trie: %v", err)
}
@@ -209,7 +209,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) {
}
srcTriedb2.Close()
- bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb)
+ bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb, 8)
if err != nil {
t.Fatalf("failed to reload binary trie after deletion: %v", err)
}
diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go
index 0aacb0878a..98ed348d8c 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -325,7 +325,7 @@ func dumpGenesis(ctx *cli.Context) error {
var genesis *core.Genesis
if utils.IsNetworkPreset(ctx) {
genesis = utils.MakeGenesis(ctx)
- } else if ctx.IsSet(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) {
+ } else if ctx.Bool(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) {
genesis = core.DeveloperGenesisBlock(11_500_000, nil)
}
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index 8e2db32d76..c02e307bdc 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/eth/ethconfig"
+ "github.com/ethereum/go-ethereum/eth/syncer"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/telemetry/tracesetup"
"github.com/ethereum/go-ethereum/internal/version"
@@ -269,25 +270,28 @@ func makeFullNode(ctx *cli.Context) *node.Node {
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
// Configure GraphQL if requested.
- if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
+ if ctx.Bool(utils.GraphQLEnabledFlag.Name) {
utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
}
// Add the Ethereum Stats daemon if requested.
if cfg.Ethstats.URL != "" {
utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL)
}
+
// Configure synchronization override service
- var synctarget common.Hash
+ syncConfig := syncer.Config{
+ ExitWhenSynced: ctx.Bool(utils.ExitWhenSyncedFlag.Name),
+ }
if ctx.IsSet(utils.SyncTargetFlag.Name) {
target := ctx.String(utils.SyncTargetFlag.Name)
if !common.IsHexHash(target) {
utils.Fatalf("sync target hash is not a valid hex hash: %s", target)
}
- synctarget = common.HexToHash(target)
+ syncConfig.TargetBlock = common.HexToHash(target)
}
- utils.RegisterSyncOverrideService(stack, eth, synctarget, ctx.Bool(utils.ExitWhenSyncedFlag.Name))
+ utils.RegisterSyncOverrideService(stack, eth, syncConfig)
- if ctx.IsSet(utils.DeveloperFlag.Name) {
+ if ctx.Bool(utils.DeveloperFlag.Name) {
// Start dev mode.
simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), cfg.Eth.Miner.PendingFeeRecipient, eth)
if err != nil {
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index ae869ec970..850e26d161 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -22,13 +22,10 @@ import (
"os"
"slices"
"sort"
- "time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/cmd/utils"
- "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console/prompt"
- "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
@@ -95,6 +92,7 @@ var (
utils.StateHistoryFlag,
utils.TrienodeHistoryFlag,
utils.TrienodeHistoryFullValueCheckpointFlag,
+ utils.BinTrieGroupDepthFlag,
utils.LightKDFFlag,
utils.EthRequiredBlocksFlag,
utils.LegacyWhitelistFlag, // deprecated
@@ -386,28 +384,4 @@ func startNode(ctx *cli.Context, stack *node.Node, isConsole bool) {
}
}
}()
-
- // Spawn a standalone goroutine for status synchronization monitoring,
- // close the node when synchronization is complete if user required.
- if ctx.Bool(utils.ExitWhenSyncedFlag.Name) {
- go func() {
- sub := stack.EventMux().Subscribe(downloader.DoneEvent{})
- defer sub.Unsubscribe()
- for {
- event := <-sub.Chan()
- if event == nil {
- continue
- }
- done, ok := event.Data.(downloader.DoneEvent)
- if !ok {
- continue
- }
- if timestamp := time.Unix(int64(done.Latest.Time), 0); time.Since(timestamp) < 10*time.Minute {
- log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(),
- "age", common.PrettyAge(timestamp))
- stack.Close()
- }
- }
- }()
- }
}
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 9d996f15cb..c41cf4ee40 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -297,6 +297,12 @@ var (
Value: ethconfig.Defaults.EnableStateSizeTracking,
Category: flags.StateCategory,
}
+ BinTrieGroupDepthFlag = &cli.IntFlag{
+ Name: "bintrie.groupdepth",
+ Usage: "Number of levels per serialized group in binary trie (1-8, default 5). Lower values create smaller groups with more nodes.",
+ Value: 5,
+ Category: flags.StateCategory,
+ }
StateHistoryFlag = &cli.Uint64Flag{
Name: "history.state",
Usage: "Number of recent blocks to retain state history for, only relevant in state.scheme=path (default = 90,000 blocks, 0 = entire chain)",
@@ -1098,7 +1104,7 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
RPCTelemetrySampleRatioFlag = &cli.Float64Flag{
Name: "rpc.telemetry.sample-ratio",
Usage: "Defines the sampling ratio for RPC telemetry (0.0 to 1.0)",
- Value: 1.0,
+ Value: node.DefaultConfig.OpenTelemetry.SampleRatio,
Category: flags.APICategory,
}
// Era flags are a group of flags related to the era archive format.
@@ -1817,6 +1823,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(TrienodeHistoryFullValueCheckpointFlag.Name) {
cfg.NodeFullValueCheckpoint = uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name))
}
+ if ctx.IsSet(BinTrieGroupDepthFlag.Name) {
+ cfg.BinTrieGroupDepth = ctx.Int(BinTrieGroupDepthFlag.Name)
+ }
if ctx.IsSet(StateSchemeFlag.Name) {
cfg.StateScheme = ctx.String(StateSchemeFlag.Name)
}
@@ -2228,13 +2237,13 @@ func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconf
}
// RegisterSyncOverrideService adds the synchronization override service into node.
-func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, target common.Hash, exitWhenSynced bool) {
- if target != (common.Hash{}) {
- log.Info("Registered sync override service", "hash", target, "exitWhenSynced", exitWhenSynced)
+func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, config syncer.Config) {
+ if config.TargetBlock != (common.Hash{}) {
+ log.Info("Registered sync override service", "hash", config.TargetBlock, "exitWhenSynced", config.ExitWhenSynced)
} else {
log.Info("Registered sync override service")
}
- syncer.Register(stack, eth, target, exitWhenSynced)
+ syncer.Register(stack, eth, config)
}
// SetupMetrics configures the metrics system.
@@ -2433,6 +2442,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
StateHistory: ctx.Uint64(StateHistoryFlag.Name),
TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name),
NodeFullValueCheckpoint: uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)),
+ BinTrieGroupDepth: ctx.Int(BinTrieGroupDepthFlag.Name),
// Disable transaction indexing/unindexing.
TxLookupLimit: -1,
diff --git a/common/hexutil/json.go b/common/hexutil/json.go
index 6b9f412078..c00cd879c8 100644
--- a/common/hexutil/json.go
+++ b/common/hexutil/json.go
@@ -204,6 +204,10 @@ func (b *Big) ToInt() *big.Int {
return (*big.Int)(b)
}
+func (b *Big) ToUint256() (*uint256.Int, bool) {
+ return uint256.FromBig((*big.Int)(b))
+}
+
// String returns the hex encoding of b.
func (b *Big) String() string {
return EncodeBig(b.ToInt())
diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go
index 72ac75c036..4237418e73 100644
--- a/consensus/beacon/consensus.go
+++ b/consensus/beacon/consensus.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
@@ -342,9 +343,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H
}
// Finalize implements consensus.Engine and processes withdrawals on top.
-func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
if !beacon.IsPoSHeader(header) {
- beacon.ethone.Finalize(chain, header, state, body)
+ beacon.ethone.Finalize(chain, header, state, body, blockAccessIndex, bal)
return
}
// Withdrawals processing.
@@ -352,7 +353,20 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
// Convert amount from gwei to wei.
amount := new(uint256.Int).SetUint64(w.Amount)
amount = amount.Mul(amount, uint256.NewInt(params.GWei))
- state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)
+ prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)
+
+ // Populate the block-level accessList if Amsterdam is enabled
+ if chain.Config().IsAmsterdam(header.Number, header.Time) {
+ if w.Amount == 0 {
+ // Zero amount withdrawal, account is accessed potential
+ // without state changes.
+ bal.AccountRead(w.Address)
+ } else {
+ // Non-zero amount withdrawal, account is accessed with
+ // a balance change.
+ bal.BalanceChange(blockAccessIndex, w.Address, new(uint256.Int).Add(&prev, amount))
+ }
+ }
}
// No block reward which is issued by consensus layer instead.
}
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index ceaec44656..f44afde241 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -34,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/keccak"
@@ -573,7 +574,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header
// Finalize implements consensus.Engine. There is no post-transaction
// consensus rules in clique, do nothing here.
-func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
// No block rewards in PoA, so the state remains as is
}
diff --git a/consensus/consensus.go b/consensus/consensus.go
index 4ba389292f..e4f7b7a6a1 100644
--- a/consensus/consensus.go
+++ b/consensus/consensus.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
)
@@ -79,12 +80,12 @@ type Engine interface {
// rules of a particular engine. The changes are executed inline.
Prepare(chain ChainHeaderReader, header *types.Header) error
- // Finalize runs any post-transaction state modifications (e.g. block rewards
- // or process withdrawals) but does not assemble the block.
+ // Finalize runs any post-transaction consensus-specific state modifications
+ // (e.g. block rewards or process withdrawals) but does not assemble the block.
//
// Note: The state database might be updated to reflect any consensus rules
// that happen at finalization (e.g. block rewards).
- Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body)
+ Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList)
// Seal generates a new sealing request for the given input block and pushes
// the result into the given channel.
diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go
index ee9d9d97d6..21adc9d279 100644
--- a/consensus/ethash/consensus.go
+++ b/consensus/ethash/consensus.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/params"
@@ -504,7 +505,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H
}
// Finalize implements consensus.Engine, accumulating the block and uncle rewards.
-func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) {
+func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) {
// Accumulate any block and uncle rewards
accumulateRewards(chain.Config(), state, header, body.Uncles)
}
diff --git a/core/bal_test.go b/core/bal_test.go
new file mode 100644
index 0000000000..f0b9dc6443
--- /dev/null
+++ b/core/bal_test.go
@@ -0,0 +1,1319 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "maps"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+// EIP-7928 BAL inclusion tests.
+//
+// Each test exercises a single rule from the spec and asserts both presence
+// and absence in the resulting block access list.
+
+// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis.
+func balChainConfig() *params.ChainConfig {
+ cfg := *params.MergedTestChainConfig
+ cfg.AmsterdamTime = new(uint64)
+ blob := *cfg.BlobScheduleConfig
+ blob.Amsterdam = blob.Osaka
+ cfg.BlobScheduleConfig = &blob
+ return &cfg
+}
+
+// balTestEnv bundles common identities used across the tests.
+type balTestEnv struct {
+ cfg *params.ChainConfig
+ signer types.Signer
+ key *ecdsa.PrivateKey
+ from common.Address
+ gspec *Genesis
+}
+
+// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys
+// the EIP-7928 system contracts. Extra accounts can be merged into Alloc.
+func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv {
+ cfg := balChainConfig()
+ key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ from := crypto.PubkeyToAddress(key.PublicKey)
+
+ alloc := types.GenesisAlloc{
+ from: {Balance: newGwei(1_000_000_000)},
+ params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0},
+ params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
+ params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
+ params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
+ }
+ maps.Copy(alloc, extra)
+ return &balTestEnv{
+ cfg: cfg,
+ signer: types.LatestSigner(cfg),
+ key: key,
+ from: from,
+ gspec: &Genesis{Config: cfg, Alloc: alloc},
+ }
+}
+
+// run generates exactly one Amsterdam block and returns its BAL.
+func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) {
+ t.Helper()
+ engine := beacon.New(ethash.NewFaker())
+ _, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) {
+ gen(b)
+ })
+ if blocks[0].AccessList() == nil {
+ t.Fatal("expected non-nil block access list")
+ }
+ return blocks[0].AccessList(), receipts[0]
+}
+
+// --- assertion helpers ---
+
+func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
+ for i := range *b {
+ if (*b)[i].Address == addr {
+ return &(*b)[i]
+ }
+ }
+ return nil
+}
+
+func hasSlotIn(slots []*uint256.Int, key common.Hash) bool {
+ want := new(uint256.Int).SetBytes(key[:])
+ for _, s := range slots {
+ if s.Cmp(want) == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool {
+ aa := findAccount(b, addr)
+ if aa == nil {
+ return false
+ }
+ want := new(uint256.Int).SetBytes(key[:])
+ for _, w := range aa.StorageWrites {
+ if w.Slot.Cmp(want) == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess {
+ t.Helper()
+ aa := findAccount(b, addr)
+ if aa == nil {
+ t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint())
+ }
+ return aa
+}
+
+func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) {
+ t.Helper()
+ if findAccount(b, addr) != nil {
+ t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint())
+ }
+}
+
+func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
+ t.Helper()
+ if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 ||
+ len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 {
+ t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa)
+ }
+}
+
+// --- tx builders ---
+
+func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction {
+ return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{
+ ChainID: e.cfg.ChainID,
+ Nonce: nonce,
+ To: to,
+ Value: value,
+ Gas: gas,
+ GasFeeCap: newGwei(10),
+ GasTipCap: newGwei(tipGwei),
+ Data: data,
+ })
+}
+
+// ============================== Account inclusion ==============================
+
+// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender
+// and a balance entry for the recipient.
+func TestBALTxSenderAndRecipient(t *testing.T) {
+ to := common.HexToAddress("0xc0ffee")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil))
+ })
+
+ sender := assertPresent(t, b, env.from)
+ if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges)
+ }
+ if len(sender.BalanceChanges) == 0 {
+ t.Fatalf("sender missing balance change")
+ }
+ recipient := assertPresent(t, b, to)
+ if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 {
+ t.Fatalf("recipient balance: %+v", recipient.BalanceChanges)
+ }
+}
+
+// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient,
+// but without a balance entry.
+func TestBALZeroValueRecipient(t *testing.T) {
+ to := common.HexToAddress("0x0123456789abcdef")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
+ })
+
+ r := assertPresent(t, b, to)
+ if len(r.BalanceChanges) != 0 {
+ t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges)
+ }
+}
+
+// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals)
+// never touches the coinbase, so it must NOT appear in the BAL — the zero
+// block reward alone does not trigger inclusion.
+func TestBALEmptyBlockExcludesCoinbase(t *testing.T) {
+ coinbase := common.Address{0xc0}
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ // SetCoinbase initialises b.bal but does not record any access.
+ g.SetCoinbase(coinbase)
+ })
+ assertAbsent(t, b, coinbase)
+}
+
+// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase
+// and the balance change appears in the BAL.
+func TestBALCoinbaseTipCapturesBalance(t *testing.T) {
+ coinbase := common.Address{0xc0}
+ to := common.HexToAddress("0xabba")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(coinbase)
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil))
+ })
+
+ cb := assertPresent(t, b, coinbase)
+ if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 {
+ t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges)
+ }
+}
+
+// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL
+// for a regular block.
+func TestBALSystemAddressExcluded(t *testing.T) {
+ to := common.HexToAddress("0xabba")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
+ })
+ assertAbsent(t, b, params.SystemAddress)
+}
+
+// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular
+// account in the BAL once it experiences state access (here: receives value).
+func TestBALSystemAddressIncludedWhenTouched(t *testing.T) {
+ sys := params.SystemAddress
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil))
+ })
+
+ aa := assertPresent(t, b, sys)
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 {
+ t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked
+// indirectly — via STATICCALL from a regular contract — must still appear in
+// the BAL with no balance entry.
+func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ caller := common.HexToAddress("0xca11")
+ // PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff)
+ // PUSH20 0x04 GAS STATICCALL POP STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, identity.Bytes()...)
+ code = append(code, 0x5a, 0xfa, 0x50, 0x00)
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile
+// with zero value lists the precompile but records no balance entry.
+func TestBALPrecompileCalledNoValueIncluded(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad}))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only
+// in the form of a value transfer — the balance entry is then recorded.
+func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) {
+ identity := common.BytesToAddress([]byte{0x04})
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, identity)
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 {
+ t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address
+// still adds it to the BAL with an empty change set.
+func TestBALBalanceProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE")
+ caller := common.HexToAddress("0xc1")
+ code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe
+ code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated
+// address adds it to the BAL with an empty change set.
+func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) {
+ probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed")
+ caller := common.HexToAddress("0xc1")
+ // PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset)
+ // PUSH20 probe EXTCODECOPY STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, probe.Bytes()...)
+ code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP
+
+ env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, probe))
+}
+
+// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is
+// never actually touched must NOT appear in the BAL.
+func TestBALAccessListNotAutoPromoted(t *testing.T) {
+ to := common.HexToAddress("0xabba")
+ dormant := common.HexToAddress("0xd0d0")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{
+ ChainID: env.cfg.ChainID,
+ Nonce: 0,
+ To: &to,
+ Value: big.NewInt(0),
+ Gas: params.TxGas + 4000,
+ GasFeeCap: newGwei(10),
+ GasTipCap: newGwei(0),
+ AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}},
+ })
+ g.AddTx(tx)
+ })
+
+ assertAbsent(t, b, dormant)
+}
+
+// ============================== CALL family ==============================
+
+// makeStubCaller emits a single CALL-family op against `target` then STOPs,
+// with zero call data and discarded return data.
+//
+// op = 0xf1 (CALL) / 0xf2 (CALLCODE):
+// stack = retSize, retOff, argsSize, argsOff, value, addr, gas
+// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL):
+// stack = retSize, retOff, argsSize, argsOff, addr, gas
+func makeStubCaller(op byte, target common.Address) []byte {
+ // retSize, retOff, argsSize, argsOff = 0
+ prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
+ if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0
+ prelude = append(prelude, 0x60, 0x00)
+ }
+ prelude = append(prelude, 0x73) // PUSH20
+ prelude = append(prelude, target.Bytes()...)
+ prelude = append(prelude, 0x5a) // GAS
+ prelude = append(prelude, op)
+ prelude = append(prelude, 0x50, 0x00) // POP, STOP
+ return prelude
+}
+
+// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing
+// contract that has no state changes lists the target with empty entries.
+func TestBALCallTargetWithEmptyChangeSet(t *testing.T) {
+ target := common.HexToAddress("0xbabe")
+ env := newBALTestEnv(types.GenesisAlloc{
+ target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an
+// empty change set (CALLCODE executes target's code in the caller's storage
+// context, so the target itself records no state changes).
+func TestBALCallCodeTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target
+// in the BAL even when neither produces state changes.
+func TestBALDelegateCallTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with
+// no balance entry recorded.
+func TestBALStaticCallTargetIncluded(t *testing.T) {
+ target := common.HexToAddress("0xdeed")
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0},
+ target: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ assertPresent(t, b, caller)
+ assertEmpty(t, assertPresent(t, b, target))
+}
+
+// ============================== Revert behaviour ==============================
+
+// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still
+// records the touched contract in the BAL with an empty change set.
+func TestBALRevertedTxStillIncluded(t *testing.T) {
+ reverter := common.HexToAddress("0xbeef")
+ // PUSH1 0 PUSH1 0 REVERT
+ revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+ env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, reverter))
+}
+
+// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the
+// sender's final nonce and balance MUST be recorded.
+func TestBALSenderRecordedOnRevert(t *testing.T) {
+ reverter := common.HexToAddress("0xbeef")
+ revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+ env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ sender := assertPresent(t, b, env.from)
+ if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges)
+ }
+ if len(sender.BalanceChanges) == 0 {
+ t.Fatalf("sender balance change (gas paid) must be present on revert")
+ }
+}
+
+// ============================== Storage inclusion ==============================
+
+// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and
+// keeps it out of storage_reads.
+func TestBALStorageWriteRecorded(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x01))
+ // PUSH1 0x42 PUSH1 0x01 SSTORE STOP
+ code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint())
+ }
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("slot 0x01 must NOT appear in storage_reads")
+ }
+}
+
+// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads.
+func TestBALStorageSloadOnly(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x07))
+ // PUSH1 0x07 SLOAD POP STOP
+ code := []byte{0x60, 0x07, 0x54, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint())
+ }
+ if hasStorageWrite(b, contract, slot) {
+ t.Fatalf("slot must NOT appear in storage_changes")
+ }
+}
+
+// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the
+// same slot drops the slot from storage_reads (write-wins invariant).
+func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP
+ code := []byte{
+ 0x60, 0x05, 0x54, 0x50,
+ 0x60, 0x42, 0x60, 0x05, 0x55,
+ 0x00,
+ }
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint())
+ }
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint())
+ }
+}
+
+// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed
+// value lands the slot in storage_reads only.
+func TestBALNoOpSSTOREDemotesToRead(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x09))
+ // SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op.
+ code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ contract: {
+ Code: code,
+ Balance: common.Big0,
+ Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
+ },
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint())
+ }
+ if hasStorageWrite(b, contract, slot) {
+ t.Fatalf("no-op SSTORE must NOT register a write")
+ }
+}
+
+// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a
+// state change and lands in storage_changes.
+func TestBALStorageWriteZeroIsAWrite(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x03))
+ // PUSH1 0 PUSH1 3 SSTORE STOP
+ code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ contract: {
+ Code: code,
+ Balance: common.Big0,
+ Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))},
+ },
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasStorageWrite(b, contract, slot) {
+ t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint())
+ }
+ for _, w := range aa.StorageWrites {
+ if w.Slot.Uint64() == 0x03 {
+ if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() {
+ t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses)
+ }
+ }
+ }
+}
+
+// ============================== CREATE / contract deployment ==============================
+
+// TestBALCreateDeploysCode: a successful contract-creation tx records the new
+// address with nonce 0→1, a balance entry (value transferred), and a code entry.
+func TestBALCreateDeploysCode(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Init: deploy runtime [0x00] (single STOP byte).
+ // PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ aa := assertPresent(t, b, created)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) {
+ t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges)
+ }
+ if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 {
+ t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges)
+ }
+}
+
+// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the
+// new address is still listed with nonce 0→1 but no code entry.
+func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ aa := assertPresent(t, b, created)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be
+// contract address is in the BAL with an empty change set.
+func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // PUSH1 0 PUSH1 0 REVERT
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ assertEmpty(t, assertPresent(t, b, created))
+}
+
+// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves
+// the deployed address in the BAL with an empty change set.
+func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG.
+ init := []byte{0x5b, 0x60, 0x00, 0x56}
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ assertEmpty(t, assertPresent(t, b, created))
+}
+
+// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address
+// that already holds a contract, the deployment fails but the address was
+// probed during execution and MUST appear in the BAL with an empty change set.
+func TestBALCreateAddressCollisionStillIncluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // For a top-level CREATE tx the deployed address is CreateAddress(sender, 0).
+ // Pre-allocate a contract at that address to provoke ErrContractAddressCollision.
+ collide := crypto.CreateAddress(env.from, 0)
+ env.gspec.Alloc[collide] = types.Account{
+ Nonce: 1,
+ Code: []byte{0x00},
+ Balance: common.Big0,
+ }
+
+ // Init code doesn't matter — execution never starts.
+ init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ aa := assertPresent(t, b, collide)
+ // The address must be present but the pre-existing nonce/code MUST NOT
+ // be overwritten by the failed creation.
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("collision must not write code: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame
+// aborts BEFORE the destination is read from state (here: the caller has 0
+// balance and CREATE requests value > 0, tripping evm.create's CanTransfer
+// check before GetCodeHash), the would-be address MUST NOT appear in the
+// BAL — only "if target account is accessed" qualifies for inclusion.
+func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
+ factory := common.HexToAddress("0xfac4")
+ // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ // The address that WOULD have been deployed had the create succeeded.
+ wouldBeDest := crypto.CreateAddress(factory, 1)
+ assertAbsent(t, b, wouldBeDest)
+
+ // The factory itself is in BAL (it ran), but its nonce MUST NOT have been
+ // bumped because evm.create returned before the SetNonce call.
+ aa := assertPresent(t, b, factory)
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("factory nonce must not be bumped on pre-access abort: %+v", aa.NonceChanges)
+ }
+}
+
+// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract
+// (not a top-level CREATE tx) records the deployed address in the BAL.
+func TestBALInEVMCreateDeploysContract(t *testing.T) {
+ factory := common.HexToAddress("0xfac4")
+ // Factory code:
+ // Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0.
+ // Then CREATE(value=0, offset=0, length=5).
+ //
+ // Layout: store the init code as a single 32-byte word at offset 0 via MSTORE
+ // with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5.
+ initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
+ var word [32]byte
+ copy(word[32-len(initBlob):], initBlob)
+ code := []byte{0x7f} // PUSH32
+ code = append(code, word[:]...)
+ code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE
+ // CREATE expects [value, offset, length] with value on bottom of stack.
+ code = append(code,
+ 0x60, 0x05, // PUSH1 5 (length)
+ 0x60, 0x1b, // PUSH1 27 (offset)
+ 0x60, 0x00, // PUSH1 0 (value)
+ 0xf0, // CREATE
+ 0x00, // STOP (discard result)
+ )
+
+ env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil))
+ })
+
+ // Deployed address depends on the factory's nonce at the moment of CREATE,
+ // which is the factory's genesis nonce (1).
+ deployed := crypto.CreateAddress(factory, 1)
+ aa := assertPresent(t, b, deployed)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 {
+ t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges)
+ }
+}
+
+// ============================== SELFDESTRUCT ==============================
+
+// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh
+// beneficiary when the destructing account has 0 balance — both addresses are
+// listed with empty change sets (no balance entry).
+func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) {
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ env := newBALTestEnv(nil)
+ // Init code performs SELFDESTRUCT to beneficiary inside the constructor,
+ // so EIP-6780's same-tx requirement is satisfied. The destructing account
+ // starts with balance 0 because the creation tx sends 0 value.
+ // PUSH20 SELFDESTRUCT
+ init := append([]byte{0x73}, beneficiary.Bytes()...)
+ init = append(init, 0xff)
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ })
+
+ created := receipts[0].ContractAddress
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 0 {
+ t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges)
+ }
+ cc := assertPresent(t, b, created)
+ if len(cc.BalanceChanges) != 0 {
+ t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges)
+ }
+}
+
+// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly
+// created contract that received positive value — beneficiary records the
+// credit; destructing account's balance entry is omitted because its
+// pre-transaction balance was 0.
+func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) {
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ env := newBALTestEnv(nil)
+ // Init code: PUSH20 SELFDESTRUCT
+ init := append([]byte{0x73}, beneficiary.Bytes()...)
+ init = append(init, 0xff)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init))
+ })
+
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 {
+ t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges)
+ }
+}
+
+// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing
+// contract with positive balance records balance→0 for the contract and the
+// corresponding credit on the beneficiary. EIP-6780 means the contract is
+// only credited and not deleted, but its balance moves regardless.
+func TestBALSelfDestructPreExistingContract(t *testing.T) {
+ suicidal := common.HexToAddress("0x5e1f")
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ // PUSH20 SELFDESTRUCT
+ code := append([]byte{0x73}, beneficiary.Bytes()...)
+ code = append(code, 0xff)
+
+ env := newBALTestEnv(types.GenesisAlloc{
+ suicidal: {Code: code, Balance: big.NewInt(50)},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, suicidal)
+ if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() {
+ t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges)
+ }
+ ben := assertPresent(t, b, beneficiary)
+ if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 {
+ t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges)
+ }
+}
+
+// ============================== Mid-tx balance round-trip ==============================
+
+// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a
+// transaction but returns to its pre-transaction value, the address is still
+// listed in the BAL but MUST NOT have a balance entry.
+func TestBALMidTxBalanceRoundTrip(t *testing.T) {
+ bouncer := common.HexToAddress("0xb0unce")
+ // On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE
+ // and zero data. Net effect: bouncer.balance returns to its pre-tx value.
+ //
+ // PUSH1 0 (retSize)
+ // PUSH1 0 (retOff)
+ // PUSH1 0 (argsSize)
+ // PUSH1 0 (argsOff)
+ // CALLVALUE
+ // CALLER
+ // GAS
+ // CALL
+ // POP
+ // STOP
+ code := []byte{
+ 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00,
+ 0x34, // CALLVALUE
+ 0x33, // CALLER
+ 0x5a, // GAS
+ 0xf1, // CALL
+ 0x50, // POP
+ 0x00, // STOP
+ }
+ env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, bouncer)
+ if len(aa.BalanceChanges) != 0 {
+ t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges)
+ }
+}
+
+// ============================== System contracts (pre/post-execution) ==============================
+
+// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses
+// accessed during pre/post-execution" MUST be included in the BAL. That
+// means all four of the post-merge system contracts touched by every
+// Amsterdam block:
+//
+// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set)
+// - EIP-2935 history storage (pre-execution)
+// - EIP-7002 withdrawal queue (post-execution)
+// - EIP-7251 consolidation queue (post-execution)
+func TestBALSystemContractsPresent(t *testing.T) {
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ // SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788.
+ g.SetCoinbase(common.Address{0xc0})
+ g.SetParentBeaconRoot(common.Hash{0xbe, 0xac})
+ })
+
+ for _, sys := range []struct {
+ name string
+ addr common.Address
+ }{
+ {"BeaconRoots (4788)", params.BeaconRootsAddress},
+ {"HistoryStorage (2935)", params.HistoryStorageAddress},
+ {"WithdrawalQueue (7002)", params.WithdrawalQueueAddress},
+ {"ConsolidationQueue (7251)", params.ConsolidationQueueAddress},
+ } {
+ if findAccount(b, sys.addr) == nil {
+ t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint())
+ }
+ }
+}
+
+// ============================== Withdrawals ==============================
+
+// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts
+// the recipient in the BAL (with no balance entry).
+func TestBALWithdrawalZeroAmountIncluded(t *testing.T) {
+ recipient := common.HexToAddress("0xdada")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(common.Address{0xc0})
+ g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0})
+ })
+
+ r := assertPresent(t, b, recipient)
+ if len(r.BalanceChanges) != 0 {
+ t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges)
+ }
+}
+
+// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal
+// records a balance change for the recipient.
+func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) {
+ recipient := common.HexToAddress("0xdada")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetCoinbase(common.Address{0xc0})
+ g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7})
+ })
+
+ r := assertPresent(t, b, recipient)
+ if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 {
+ t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges)
+ }
+}
+
+// ============================== EIP-7702 authority ==============================
+
+// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code
+// transaction is added to the BAL once its delegation is loaded, recording
+// both the nonce bump and the delegation-pointer code entry.
+func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegate := common.HexToAddress("0xdeadbeef")
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(env.cfg.ChainID),
+ Nonce: 0,
+ To: env.from,
+ Value: new(uint256.Int),
+ Gas: 200_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: []types.SetCodeAuthorization{auth},
+ })
+ g.AddTx(tx)
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) == 0 {
+ t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) == 0 {
+ t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target
+// MUST NOT appear in the BAL when only the authorization is installed and the
+// target is never loaded as an execution target.
+func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ delegate := common.HexToAddress("0xdeadbeef") // never accessed
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(env.cfg.ChainID),
+ Nonce: 0,
+ To: env.from, // tx.to is an EOA with no code: delegate is never called
+ Value: new(uint256.Int),
+ Gas: 200_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: []types.SetCodeAuthorization{auth},
+ })
+ g.AddTx(tx)
+ })
+
+ assertAbsent(t, b, delegate)
+}
+
+// newSetCodeTx is a small constructor used by the multi-auth tests below.
+func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
+ t.Helper()
+ tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{
+ ChainID: uint256.MustFromBig(e.cfg.ChainID),
+ Nonce: nonce,
+ To: to,
+ Value: new(uint256.Int),
+ Gas: 400_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: auths,
+ }), e.signer, e.key)
+ if err != nil {
+ t.Fatalf("sign SetCodeTx: %v", err)
+ }
+ return tx
+}
+
+// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check
+// fails returns before the authority is loaded, so the authority address
+// MUST NOT appear in the BAL.
+func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load)
+ Address: common.HexToAddress("0xdeadbeef"),
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
+ })
+
+ assertAbsent(t, b, authority)
+}
+
+// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the
+// nonce check happens AFTER the authority's code is loaded (and the address
+// added to accessed_addresses), so the authority MUST appear in the BAL —
+// but with no nonce or code change.
+func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+
+ // The authority's actual nonce is 0; supplying auth.Nonce=99 makes
+ // validation fail only after the code has been loaded.
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: common.HexToAddress("0xdeadbeef"),
+ Nonce: 99,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and
+// pre-load-failed auths lists only the loaded authorities in the BAL.
+func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030")
+ good := crypto.PubkeyToAddress(goodKey.PublicKey)
+ bad := crypto.PubkeyToAddress(badKey.PublicKey)
+ delegate := common.HexToAddress("0xdeadbeef")
+
+ goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign good auth: %v", err)
+ }
+ badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), // fails before load
+ Address: delegate,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign bad auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth}))
+ })
+
+ assertPresent(t, b, good) // loaded → in BAL
+ assertAbsent(t, b, bad) // never loaded → not in BAL
+}
+
+// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that
+// (1) install a delegation and (2) clear it again. Final code equals pre-tx
+// code (empty), so the BAL records only the cumulative nonce bump and NO
+// code change.
+func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegateA := common.HexToAddress("0xa11ce")
+
+ auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateA, // empty → A
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth1: %v", err)
+ }
+ auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: common.Address{}, // delegation to zero clears the code (A → empty)
+ Nonce: 1,
+ })
+ if err != nil {
+ t.Fatalf("sign auth2: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
+ t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges)
+ }
+}
+
+// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority
+// switching delegation A → B record exactly one code change carrying the
+// final delegation pointer (B), not the intermediate value.
+func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) {
+ env := newBALTestEnv(nil)
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ delegateA := common.HexToAddress("0xa11ce")
+ delegateB := common.HexToAddress("0xb0b0b0")
+
+ auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateA,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth1: %v", err)
+ }
+ auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: delegateB,
+ Nonce: 1,
+ })
+ if err != nil {
+ t.Fatalf("sign auth2: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.CodeChanges) != 1 {
+ t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges)
+ }
+ want := types.AddressToDelegation(delegateB)
+ if !bytes.Equal(aa.CodeChanges[0].Code, want) {
+ t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code)
+ }
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 {
+ t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
+ }
+}
diff --git a/core/bench_test.go b/core/bench_test.go
index 20d1a7794b..65179c54d4 100644
--- a/core/bench_test.go
+++ b/core/bench_test.go
@@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
data := make([]byte, nbytes)
return func(i int, gen *BlockGen) {
toaddr := common.Address{}
- cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false)
+ cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false)
signer := gen.Signer()
gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil {
diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go
index 1b033151d3..b49ac83bb5 100644
--- a/core/bintrie_witness_test.go
+++ b/core/bintrie_witness_test.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
@@ -63,12 +64,12 @@ var (
func TestProcessUBT(t *testing.T) {
var (
code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`)
- intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true)
+ intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false)
// A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness
// will not contain that copied data.
// Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985
codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`)
- intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true)
+ intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false)
signer = types.LatestSigner(testUBTChainConfig)
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain
@@ -92,6 +93,7 @@ func TestProcessUBT(t *testing.T) {
// genesis := gspec.MustCommit(bcdb, triedb)
options := DefaultConfig().WithStateScheme(rawdb.PathScheme)
options.SnapshotLimit = 0
+ options.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth
blockchain, _ := NewBlockChain(bcdb, gspec, beacon.New(ethash.NewFaker()), options)
defer blockchain.Stop()
@@ -201,7 +203,7 @@ func TestProcessParentBlockHash(t *testing.T) {
}
vmContext := NewEVMBlockContext(header, nil, new(common.Address))
evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
- ProcessParentBlockHash(header.ParentHash, evm)
+ ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList())
}
// Read block hashes for block 0 .. num-1
for i := 0; i < num; i++ {
@@ -218,6 +220,7 @@ func TestProcessParentBlockHash(t *testing.T) {
t.Run("UBT", func(t *testing.T) {
db := rawdb.NewMemoryDatabase()
cacheConfig := DefaultConfig().WithStateScheme(rawdb.PathScheme)
+ cacheConfig.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth
cacheConfig.SnapshotLimit = 0
triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig(true))
statedb, _ := state.New(types.EmptyBinaryHash, state.NewDatabase(triedb, nil))
diff --git a/core/block_validator.go b/core/block_validator.go
index 008444fbbc..4086a2ead7 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -111,6 +111,28 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
}
}
+ // Block access list hash must be present in header after the
+ // Amsterdam hard fork.
+ if v.config.IsAmsterdam(block.Number(), block.Time()) {
+ if block.Header().BlockAccessListHash == nil {
+ return errors.New("block access list hash not set in header")
+ }
+ // If the block does not include an access list, compute it locally during
+ // execution and validate it against the access list hash in the header.
+ //
+ // If the block includes an attached access list, validate it directly here.
+ if block.AccessList() != nil {
+ computed := block.AccessList().Hash()
+ if *block.Header().BlockAccessListHash != computed {
+ return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash)
+ } else if err := block.AccessList().Validate(block.GasLimit()); err != nil {
+ return fmt.Errorf("invalid block access list: %v", err)
+ }
+ }
+ } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil {
+ return errors.New("block had access list before Amsterdam")
+ }
+
// Ancestor block must be known.
if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) {
@@ -160,6 +182,23 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
} else if res.Requests != nil {
return errors.New("block has requests before prague fork")
}
+ // Verify Block-level accessList once Amsterdam is enabled
+ if v.config.IsAmsterdam(block.Number(), block.Time()) {
+ if res.Bal == nil {
+ return errors.New("block access list is not available in amsterdam")
+ }
+ if block.Header().BlockAccessListHash == nil {
+ return errors.New("block access list hash not set in header")
+ }
+ enc := res.Bal.ToEncodingObj()
+ local, remote := enc.Hash(), *block.Header().BlockAccessListHash
+ if local != remote {
+ return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote)
+ }
+ if err := enc.Validate(block.GasLimit()); err != nil {
+ return fmt.Errorf("invalid block access list: %v", err)
+ }
+ }
// Validate the state root against the received state root and throw
// an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
diff --git a/core/blockchain.go b/core/blockchain.go
index 296ef6bc16..7b5a910b7a 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -170,9 +170,10 @@ type BlockChainConfig struct {
TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed
TrieJournalDirectory string // Directory path to the journal used for persisting trie data across node restarts
- Preimages bool // Whether to store preimage of trie key to the disk
- StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top
- ArchiveMode bool // Whether to enable the archive mode
+ Preimages bool // Whether to store preimage of trie key to the disk
+ StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top
+ ArchiveMode bool // Whether to enable the archive mode
+ BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8)
// Number of blocks from the chain head for which state histories are retained.
// If set to 0, all state histories across the entire chain will be retained;
@@ -260,8 +261,9 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig {
// triedbConfig derives the configures for trie database.
func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config {
config := &triedb.Config{
- Preimages: cfg.Preimages,
- IsUBT: isUBT,
+ Preimages: cfg.Preimages,
+ IsUBT: isUBT,
+ BinTrieGroupDepth: cfg.BinTrieGroupDepth,
}
if cfg.StateScheme == rawdb.HashScheme {
config.HashDB = &hashdb.Config{
@@ -1184,6 +1186,7 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error {
}
// If all checks out, manually set the head block.
+ rawdb.WriteHeadBlockHash(bc.db, hash)
bc.currentBlock.Store(block.Header())
headBlockGauge.Update(int64(block.NumberU64()))
@@ -2594,8 +2597,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error
blockReorgAddMeter.Mark(int64(len(newChain)))
} else {
// len(newChain) == 0 && len(oldChain) > 0
- // rewind the canonical chain to a lower point.
- log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain), "newnum", newHead.Number, "newhash", newHead.Hash(), "newblocks", len(newChain))
+ // Rewind the canonical chain to a lower point. In EPBs we can reorg to
+ // a parent of the head within 32 blocks.
+ if len(oldChain) > 32 {
+ log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain))
+ } else {
+ log.Info("Shorten chain", "del", len(oldChain), "number", oldHead.Number, "hash", oldHead.Hash())
+ }
}
// Acquire the tx-lookup lock before mutation. This step is essential
// as the txlookups should be changed atomically, and all subsequent
diff --git a/core/chain_makers.go b/core/chain_makers.go
index 46cd98de61..2e856b5161 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -17,6 +17,7 @@
package core
import (
+ "context"
"fmt"
"math/big"
@@ -28,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/params"
@@ -49,6 +51,7 @@ type BlockGen struct {
receipts []*types.Receipt
uncles []*types.Header
withdrawals []*types.Withdrawal
+ bal *bal.ConstructionBlockAccessList
engine consensus.Engine
}
@@ -98,7 +101,7 @@ func (b *BlockGen) Difficulty() *big.Int {
func (b *BlockGen) SetParentBeaconRoot(root common.Hash) {
b.header.ParentBeaconRoot = &root
blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
- ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}))
+ ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}), b.bal)
}
// addTx adds a transaction to the generated block. If no coinbase has
@@ -116,8 +119,8 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase)
evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig)
)
- b.statedb.SetTxContext(tx.Hash(), len(b.txs))
- receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
+ b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1))
+ receipt, bal, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
if err != nil {
panic(err)
}
@@ -133,6 +136,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
if b.header.BlobGasUsed != nil {
*b.header.BlobGasUsed += receipt.BlobGasUsed
}
+ b.bal.Merge(bal)
}
// AddTx adds a transaction to the generated block. If no coinbase has
@@ -303,10 +307,11 @@ func (b *BlockGen) OffsetTime(seconds int64) {
// ConsensusLayerRequests returns the EIP-7685 requests which have accumulated so far.
func (b *BlockGen) ConsensusLayerRequests() [][]byte {
- return b.collectRequests(true)
+ requests, _ := b.collectRequests(true)
+ return requests
}
-func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
+func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.ConstructionBlockAccessList) {
statedb := b.statedb
if readonly {
// The system contracts clear themselves on a system-initiated read.
@@ -314,30 +319,19 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
// off the statedb before executing the system calls.
statedb = statedb.Copy()
}
-
- if b.cm.config.IsPrague(b.header.Number, b.header.Time) {
- requests = [][]byte{}
- // EIP-6110 deposits
- var blockLogs []*types.Log
- for _, r := range b.receipts {
- blockLogs = append(blockLogs, r.Logs...)
- }
- if err := ParseDepositLogs(&requests, blockLogs, b.cm.config); err != nil {
- panic(fmt.Sprintf("failed to parse deposit log: %v", err))
- }
- // create EVM for system calls
- blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
- evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
- // EIP-7002
- if err := ProcessWithdrawalQueue(&requests, evm); err != nil {
- panic(fmt.Sprintf("could not process withdrawal requests: %v", err))
- }
- // EIP-7251
- if err := ProcessConsolidationQueue(&requests, evm); err != nil {
- panic(fmt.Sprintf("could not process consolidation requests: %v", err))
- }
+ var blockLogs []*types.Log
+ for _, r := range b.receipts {
+ blockLogs = append(blockLogs, r.Logs...)
}
- return requests
+ // TODO use the shared EVM throughout the entire generation cycle
+ blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
+ evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
+
+ requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1))
+ if err != nil {
+ panic(fmt.Sprintf("failed to run post-execution: %v", err))
+ }
+ return requests, bal
}
// GenerateChain creates a chain of n blocks. The first block's
@@ -364,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) {
b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine}
b.header = cm.makeHeader(parent, statedb, b.engine)
+ b.bal = bal.NewConstructionBlockAccessList()
// Set the difficulty for clique block. The chain maker doesn't have access
// to a chain, so the difficulty will be left unset (nil). Set it here to the
@@ -396,7 +391,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase)
blockContext.Random = &common.Hash{} // enable post-merge instruction set
evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{})
- ProcessParentBlockHash(b.header.ParentHash, evm)
+ ProcessParentBlockHash(b.header.ParentHash, evm, b.bal)
}
// Execute any user modifications to the block
@@ -404,11 +399,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
gen(i, b)
}
- requests := b.collectRequests(false)
+ requests, bal := b.collectRequests(false)
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
b.header.RequestsHash = &reqHash
}
+ b.bal.Merge(bal)
body := types.Body{
Transactions: b.txs,
@@ -424,8 +420,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
body.Withdrawals = make([]*types.Withdrawal, 0)
}
}
+ // Apply the consensus-specific post-transaction changes
+ b.engine.Finalize(cm, b.header, statedb, &body, uint32(len(b.txs)+1), b.bal)
+
// Assemble the block for delivery.
- block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts)
+ block := AssembleBlock(cm, b.header, statedb, &body, b.receipts, b.bal)
// Write state changes to db
root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number), config.IsCancun(b.header.Number, b.header.Time))
diff --git a/core/evm.go b/core/evm.go
index 818b23bee5..73e4c01a99 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -87,7 +87,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
func NewEVMTxContext(msg *Message) vm.TxContext {
ctx := vm.TxContext{
Origin: msg.From,
- GasPrice: uint256.MustFromBig(msg.GasPrice),
+ GasPrice: msg.GasPrice,
BlobHashes: msg.BlobHashes,
}
return ctx
diff --git a/core/genesis.go b/core/genesis.go
index d77ea10d8c..e1c67e57c2 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -136,8 +136,9 @@ func hashAlloc(ga *types.GenesisAlloc, isUBT bool) (common.Hash, error) {
var config *triedb.Config
if isUBT {
config = &triedb.Config{
- PathDB: pathdb.Defaults,
- IsUBT: true,
+ PathDB: pathdb.Defaults,
+ IsUBT: true,
+ BinTrieGroupDepth: triedb.UBTDefaults.BinTrieGroupDepth,
}
}
// Create an ephemeral in-memory database for computing hash,
@@ -554,6 +555,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
if head.SlotNumber == nil {
head.SlotNumber = new(uint64)
}
+ head.BlockAccessListHash = &types.EmptyBlockAccessListHash
}
}
return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil))
diff --git a/core/genesis_test.go b/core/genesis_test.go
index e15ad00222..94f1b3a4fd 100644
--- a/core/genesis_test.go
+++ b/core/genesis_test.go
@@ -261,9 +261,9 @@ func newDbConfig(scheme string) *triedb.Config {
return &triedb.Config{PathDB: &config}
}
-func TestVerkleGenesisCommit(t *testing.T) {
- var verkleTime uint64 = 0
- verkleConfig := ¶ms.ChainConfig{
+func TestBinaryGenesisCommit(t *testing.T) {
+ var ubtTime uint64 = 0
+ ubtConfig := ¶ms.ChainConfig{
ChainID: big.NewInt(1),
HomesteadBlock: big.NewInt(0),
DAOForkBlock: nil,
@@ -281,11 +281,11 @@ func TestVerkleGenesisCommit(t *testing.T) {
ArrowGlacierBlock: big.NewInt(0),
GrayGlacierBlock: big.NewInt(0),
MergeNetsplitBlock: nil,
- ShanghaiTime: &verkleTime,
- CancunTime: &verkleTime,
- PragueTime: &verkleTime,
- OsakaTime: &verkleTime,
- UBTTime: &verkleTime,
+ ShanghaiTime: &ubtTime,
+ CancunTime: &ubtTime,
+ PragueTime: &ubtTime,
+ OsakaTime: &ubtTime,
+ UBTTime: &ubtTime,
TerminalTotalDifficulty: big.NewInt(0),
EnableUBTAtGenesis: true,
Ethash: nil,
@@ -300,8 +300,8 @@ func TestVerkleGenesisCommit(t *testing.T) {
genesis := &Genesis{
BaseFee: big.NewInt(params.InitialBaseFee),
- Config: verkleConfig,
- Timestamp: verkleTime,
+ Config: ubtConfig,
+ Timestamp: ubtTime,
Difficulty: big.NewInt(0),
Alloc: types.GenesisAlloc{
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
@@ -320,17 +320,18 @@ func TestVerkleGenesisCommit(t *testing.T) {
config.NoAsyncFlush = true
triedb := triedb.NewDatabase(db, &triedb.Config{
- IsUBT: true,
- PathDB: &config,
+ IsUBT: true,
+ PathDB: &config,
+ BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth,
})
block := genesis.MustCommit(db, triedb)
if !bytes.Equal(block.Root().Bytes(), expected) {
t.Fatalf("invalid genesis state root, expected %x, got %x", expected, block.Root())
}
- // Test that the trie is verkle
+ // Test that the trie is a unified binary trie
if !triedb.IsUBT() {
- t.Fatalf("expected trie to be verkle")
+ t.Fatalf("expected trie to be a unified binary trie")
}
vdb := rawdb.NewTable(db, string(rawdb.VerklePrefix))
if !rawdb.HasAccountTrieNode(vdb, nil) {
diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go
index 718d93df87..16579f6d6a 100644
--- a/core/state/database_ubt.go
+++ b/core/state/database_ubt.go
@@ -96,7 +96,7 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea
// OpenTrie opens the main account trie at a specific root hash.
func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) {
- return bintrie.NewBinaryTrie(root, db.triedb)
+ return bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth())
}
// OpenStorageTrie opens the storage trie of an account. In binary trie mode,
diff --git a/core/state/journal.go b/core/state/journal.go
index a79bd7331a..353144a1c7 100644
--- a/core/state/journal.go
+++ b/core/state/journal.go
@@ -18,7 +18,6 @@ package state
import (
"fmt"
- "maps"
"slices"
"sort"
@@ -32,26 +31,163 @@ type revision struct {
journalIndex int
}
+// journalMutationKind indicates the type of account mutation.
+type journalMutationKind uint8
+
+const (
+ // journalMutationKindNone is the zero value returned by mutation() for
+ // entries that don't carry a tracked account mutation. The accompanying
+ // bool is false in that case; callers must gate on it before using the
+ // kind.
+ journalMutationKindNone journalMutationKind = iota
+ journalMutationKindTouch
+ journalMutationKindCreate
+ journalMutationKindSelfDestruct
+ journalMutationKindBalance
+ journalMutationKindNonce
+ journalMutationKindCode
+ journalMutationKindStorage
+ journalMutationKindCount // sentinel, must stay last
+)
+
+type journalMutationCounts [journalMutationKindCount]int
+
+// journalMutationState tracks, per account, both the per-kind count of mutation
+// entries currently present in the journal and the pre-tx value of each
+// metadata field captured on its first touch (balance/nonce/code).
+// The *Set flags indicate whether the corresponding field has been mutated
+// at least once in the current tx window; they are cleared when all entries
+// of that kind are reverted. Storage slots are tracked elsewhere.
+type journalMutationState struct {
+ counts journalMutationCounts
+
+ balance *uint256.Int
+ balanceSet bool
+ nonce uint64
+ nonceSet bool
+ code []byte
+ codeSet bool
+}
+
+func (s *journalMutationState) add(kind journalMutationKind) {
+ s.counts.add(kind)
+}
+
+// remove drops one occurrence of the given mutation kind. It returns a flag
+// indicating whether no entries of any kind remain.
+func (s *journalMutationState) remove(kind journalMutationKind) bool {
+ if s.counts.remove(kind) {
+ // No entries of this kind remain for this account; drop the
+ // corresponding stashed original so the state mirrors the
+ // live mutation set.
+ s.clearKind(kind)
+ }
+ return s.counts == (journalMutationCounts{})
+}
+
+// clearKind drops the stashed original for the given mutation kind. It is
+// invoked during revert once no journal entries of that kind remain for the
+// account. Kinds that don't correspond to a tracked metadata field are no-ops.
+func (s *journalMutationState) clearKind(kind journalMutationKind) {
+ switch kind {
+ case journalMutationKindBalance:
+ s.balance = nil
+ s.balanceSet = false
+ case journalMutationKindNonce:
+ s.nonce = 0
+ s.nonceSet = false
+ case journalMutationKindCode:
+ s.code = nil
+ s.codeSet = false
+ }
+}
+
+func (s *journalMutationState) copy() *journalMutationState {
+ cpy := *s
+ if s.balance != nil {
+ cpy.balance = new(uint256.Int).Set(s.balance)
+ }
+ if s.code != nil {
+ cpy.code = slices.Clone(s.code)
+ }
+ return &cpy
+}
+
+func (c *journalMutationCounts) add(kind journalMutationKind) {
+ c[kind]++
+}
+
+func (c *journalMutationCounts) remove(kind journalMutationKind) bool {
+ c[kind]--
+ return c[kind] == 0
+}
+
// journalEntry is a modification entry in the state change journal that can be
// reverted on demand.
type journalEntry interface {
// revert undoes the changes introduced by this journal entry.
revert(*StateDB)
- // dirtied returns the Ethereum address modified by this journal entry.
- // indicates false if no address was changed.
- dirtied() (common.Address, bool)
+ // mutation returns the account mutation introduced by this entry.
+ // It indicates false if no tracked account mutation was made.
+ mutation() (common.Address, journalMutationKind, bool)
// copy returns a deep-copied journal entry.
copy() journalEntry
}
+// stashBalance records prev as the pre-tx balance of addr, iff this is the
+// first balance touch seen in the current tx. Subsequent balance writes are
+// ignored so the stored value remains the true pre-tx original.
+func (j *journal) stashBalance(addr common.Address, prev *uint256.Int) {
+ s := j.mutationStateFor(addr)
+ if s.balanceSet {
+ return
+ }
+ // The balance is already deep-copied and safe to hold the object here.
+ s.balance = prev
+ s.balanceSet = true
+}
+
+// stashNonce records prev as the pre-tx nonce of addr on first touch.
+func (j *journal) stashNonce(addr common.Address, prev uint64) {
+ s := j.mutationStateFor(addr)
+ if s.nonceSet {
+ return
+ }
+ s.nonce = prev
+ s.nonceSet = true
+}
+
+// stashCode records prev as the pre-tx code of addr on first touch.
+func (j *journal) stashCode(addr common.Address, prev []byte) {
+ s := j.mutationStateFor(addr)
+ if s.codeSet {
+ return
+ }
+ // The code is already deep-copied in the StateDB, safe to
+ // hold the reference here.
+ s.code = prev
+ s.codeSet = true
+}
+
+// mutationStateFor returns the mutation state for addr, creating an empty one
+// if absent.
+func (j *journal) mutationStateFor(addr common.Address) *journalMutationState {
+ s := j.mutations[addr]
+ if s == nil {
+ s = new(journalMutationState)
+ j.mutations[addr] = s
+ }
+ return s
+}
+
// journal contains the list of state modifications applied since the last state
// commit. These are tracked to be able to be reverted in the case of an execution
// exception or request for reversal.
type journal struct {
- entries []journalEntry // Current changes tracked by the journal
- dirties map[common.Address]int // Dirty accounts and the number of changes
+ entries []journalEntry // Current changes tracked by the journal
+ mutations map[common.Address]*journalMutationState // Per-account mutation kinds and pre-tx originals
validRevisions []revision
nextRevisionId int
@@ -60,7 +196,7 @@ type journal struct {
// newJournal creates a new initialized journal.
func newJournal() *journal {
return &journal{
- dirties: make(map[common.Address]int),
+ mutations: make(map[common.Address]*journalMutationState),
}
}
@@ -70,7 +206,7 @@ func newJournal() *journal {
func (j *journal) reset() {
j.entries = j.entries[:0]
j.validRevisions = j.validRevisions[:0]
- clear(j.dirties)
+ clear(j.mutations)
j.nextRevisionId = 0
}
@@ -101,33 +237,52 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) {
// append inserts a new modification entry to the end of the change journal.
func (j *journal) append(entry journalEntry) {
j.entries = append(j.entries, entry)
- if addr, dirty := entry.dirtied(); dirty {
- j.dirties[addr]++
+ if addr, kind, dirty := entry.mutation(); dirty {
+ state := j.mutations[addr]
+ if state == nil {
+ state = new(journalMutationState)
+ j.mutations[addr] = state
+ }
+ state.add(kind)
}
}
// revert undoes a batch of journalled modifications along with any reverted
-// dirty handling too.
+// mutation tracking too.
func (j *journal) revert(statedb *StateDB, snapshot int) {
for i := len(j.entries) - 1; i >= snapshot; i-- {
// Undo the changes made by the operation
j.entries[i].revert(statedb)
- // Drop any dirty tracking induced by the change
- if addr, dirty := j.entries[i].dirtied(); dirty {
- if j.dirties[addr]--; j.dirties[addr] == 0 {
- delete(j.dirties, addr)
+ // Drop any mutation tracking induced by the change.
+ if addr, kind, dirty := j.entries[i].mutation(); dirty {
+ state := j.mutations[addr]
+ if state == nil {
+ panic(fmt.Errorf("journal mutation tracking missing for %x", addr[:]))
+ }
+ if state.remove(kind) {
+ delete(j.mutations, addr)
}
}
}
j.entries = j.entries[:snapshot]
}
-// dirty explicitly sets an address to dirty, even if the change entries would
-// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD
-// precompile consensus exception.
-func (j *journal) dirty(addr common.Address) {
- j.dirties[addr]++
+// ripemdMagic explicitly keeps RIPEMD160 in the mutation set with a touch change.
+//
+// Ethereum Mainnet contains an old empty-account touch/revert quirk for address
+// 0x03. If we only relied on the journal entry above, the revert path would
+// remove the account from the mutation set together with the touch.
+//
+// Keep an explicit touch marker so tx finalisation still sees RIPEMD160
+// on the mutation pass when replaying that historical case.
+func (j *journal) ripemdMagic() {
+ state := j.mutations[ripemd]
+ if state == nil {
+ state = new(journalMutationState)
+ j.mutations[ripemd] = state
+ }
+ state.add(journalMutationKindTouch)
}
// length returns the current number of entries in the journal.
@@ -141,9 +296,13 @@ func (j *journal) copy() *journal {
for i := 0; i < j.length(); i++ {
entries = append(entries, j.entries[i].copy())
}
+ mutations := make(map[common.Address]*journalMutationState, len(j.mutations))
+ for addr, state := range j.mutations {
+ mutations[addr] = state.copy()
+ }
return &journal{
entries: entries,
- dirties: maps.Clone(j.dirties),
+ mutations: mutations,
validRevisions: slices.Clone(j.validRevisions),
nextRevisionId: j.nextRevisionId,
}
@@ -187,13 +346,16 @@ func (j *journal) refundChange(previous uint64) {
}
func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) {
+ prev := previous.Clone()
+ j.stashBalance(addr, prev)
j.append(balanceChange{
account: addr,
- prev: previous.Clone(),
+ prev: prev,
})
}
func (j *journal) setCode(address common.Address, prevCode []byte) {
+ j.stashCode(address, prevCode)
j.append(codeChange{
account: address,
prevCode: prevCode,
@@ -201,6 +363,7 @@ func (j *journal) setCode(address common.Address, prevCode []byte) {
}
func (j *journal) nonceChange(address common.Address, prev uint64) {
+ j.stashNonce(address, prev)
j.append(nonceChange{
account: address,
prev: prev,
@@ -212,9 +375,18 @@ func (j *journal) touchChange(address common.Address) {
account: address,
})
if address == ripemd {
- // Explicitly put it in the dirty-cache, which is otherwise generated from
- // flattened journals.
- j.dirty(address)
+ // Preserve the historical RIPEMD160 precompile consensus exception.
+ //
+ // Mainnet contains an old empty-account touch/revert quirk for address
+ // 0x03. If we only relied on the journal entry above, the revert path
+ // would remove the account from the dirty set together with the touch.
+ // Keep an explicit dirty marker so tx finalisation still sees the
+ // account on the dirty pass when replaying that historical case.
+ //
+ // This does not force deletion by itself: Finalise will still delete the
+ // account only if the state object is present at tx end and qualifies for
+ // deletion there.
+ j.ripemdMagic()
}
}
@@ -295,8 +467,8 @@ func (ch createObjectChange) revert(s *StateDB) {
delete(s.stateObjects, ch.account)
}
-func (ch createObjectChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch createObjectChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindCreate, true
}
func (ch createObjectChange) copy() journalEntry {
@@ -309,8 +481,8 @@ func (ch createContractChange) revert(s *StateDB) {
s.getStateObject(ch.account).newContract = false
}
-func (ch createContractChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch createContractChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch createContractChange) copy() journalEntry {
@@ -326,8 +498,8 @@ func (ch selfDestructChange) revert(s *StateDB) {
}
}
-func (ch selfDestructChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch selfDestructChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindSelfDestruct, true
}
func (ch selfDestructChange) copy() journalEntry {
@@ -341,8 +513,8 @@ var ripemd = common.HexToAddress("0000000000000000000000000000000000000003")
func (ch touchChange) revert(s *StateDB) {
}
-func (ch touchChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch touchChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindTouch, true
}
func (ch touchChange) copy() journalEntry {
@@ -355,8 +527,8 @@ func (ch balanceChange) revert(s *StateDB) {
s.getStateObject(ch.account).setBalance(ch.prev)
}
-func (ch balanceChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch balanceChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindBalance, true
}
func (ch balanceChange) copy() journalEntry {
@@ -370,8 +542,8 @@ func (ch nonceChange) revert(s *StateDB) {
s.getStateObject(ch.account).setNonce(ch.prev)
}
-func (ch nonceChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch nonceChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindNonce, true
}
func (ch nonceChange) copy() journalEntry {
@@ -385,8 +557,8 @@ func (ch codeChange) revert(s *StateDB) {
s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode)
}
-func (ch codeChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch codeChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindCode, true
}
func (ch codeChange) copy() journalEntry {
@@ -400,8 +572,8 @@ func (ch storageChange) revert(s *StateDB) {
s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue)
}
-func (ch storageChange) dirtied() (common.Address, bool) {
- return ch.account, true
+func (ch storageChange) mutation() (common.Address, journalMutationKind, bool) {
+ return ch.account, journalMutationKindStorage, true
}
func (ch storageChange) copy() journalEntry {
@@ -417,8 +589,8 @@ func (ch transientStorageChange) revert(s *StateDB) {
s.setTransientState(ch.account, ch.key, ch.prevalue)
}
-func (ch transientStorageChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch transientStorageChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch transientStorageChange) copy() journalEntry {
@@ -433,8 +605,8 @@ func (ch refundChange) revert(s *StateDB) {
s.refund = ch.prev
}
-func (ch refundChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch refundChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch refundChange) copy() journalEntry {
@@ -453,8 +625,8 @@ func (ch addLogChange) revert(s *StateDB) {
s.logSize--
}
-func (ch addLogChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch addLogChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch addLogChange) copy() journalEntry {
@@ -476,8 +648,8 @@ func (ch accessListAddAccountChange) revert(s *StateDB) {
s.accessList.DeleteAddress(ch.address)
}
-func (ch accessListAddAccountChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch accessListAddAccountChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch accessListAddAccountChange) copy() journalEntry {
@@ -490,8 +662,8 @@ func (ch accessListAddSlotChange) revert(s *StateDB) {
s.accessList.DeleteSlot(ch.address, ch.slot)
}
-func (ch accessListAddSlotChange) dirtied() (common.Address, bool) {
- return common.Address{}, false
+func (ch accessListAddSlotChange) mutation() (common.Address, journalMutationKind, bool) {
+ return common.Address{}, journalMutationKindNone, false
}
func (ch accessListAddSlotChange) copy() journalEntry {
diff --git a/core/state/journal_test.go b/core/state/journal_test.go
new file mode 100644
index 0000000000..262cee77fe
--- /dev/null
+++ b/core/state/journal_test.go
@@ -0,0 +1,219 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/holiman/uint256"
+)
+
+// fuzzJournalAddrs is a small fixed pool used by the fuzz harness to force
+// repeated collisions on the same account, which exercises the multi-entry
+// path in the journal's mutation tracking and originals cleanup on revert.
+// It deliberately excludes the RIPEMD-160 precompile (0x03), which has a
+// consensus-level touch/revert exception that would complicate invariants.
+var fuzzJournalAddrs = []common.Address{
+ common.BytesToAddress([]byte{0x11}),
+ common.BytesToAddress([]byte{0x22}),
+ common.BytesToAddress([]byte{0x44}),
+}
+
+// checkJournalInvariants validates that:
+// - journal.mutations exactly reflects the dirty entries currently in
+// journal.entries (per-kind counts and mask match what you'd get by
+// walking the entries from scratch).
+// - journal.originals mirrors that set for the three tracked metadata kinds
+// (balance/nonce/code): a *Set flag is true iff the account currently has
+// at least one corresponding entry in the journal.
+// - An address is present in originals only if it also has at least one
+// tracked-kind mutation in the journal.
+func checkJournalInvariants(t *testing.T, j *journal) {
+ t.Helper()
+
+ // Reconstruct the expected per-address counts from the live entries.
+ expected := make(map[common.Address]*journalMutationCounts)
+ for _, e := range j.entries {
+ addr, kind, dirty := e.mutation()
+ if !dirty {
+ continue
+ }
+ c := expected[addr]
+ if c == nil {
+ c = &journalMutationCounts{}
+ expected[addr] = c
+ }
+ c.add(kind)
+ }
+
+ if len(j.mutations) != len(expected) {
+ t.Fatalf("mutations size %d, want %d", len(j.mutations), len(expected))
+ }
+ for addr, state := range j.mutations {
+ want, ok := expected[addr]
+ if !ok {
+ t.Fatalf("mutations has extra address %x", addr)
+ }
+ if state.counts != *want {
+ t.Fatalf("addr %x: counts=%+v want=%+v", addr, state.counts, *want)
+ }
+ // First-touch *Set flags must mirror the live per-kind counts.
+ if state.balanceSet != (want[journalMutationKindBalance] > 0) {
+ t.Fatalf("addr %x: balanceSet=%v want=%v (balance count=%d)",
+ addr, state.balanceSet, want[journalMutationKindBalance] > 0, want[journalMutationKindBalance])
+ }
+ if state.nonceSet != (want[journalMutationKindNonce] > 0) {
+ t.Fatalf("addr %x: nonceSet=%v want=%v (nonce count=%d)",
+ addr, state.nonceSet, want[journalMutationKindNonce] > 0, want[journalMutationKindNonce])
+ }
+ if state.codeSet != (want[journalMutationKindCode] > 0) {
+ t.Fatalf("addr %x: codeSet=%v want=%v (code count=%d)",
+ addr, state.codeSet, want[journalMutationKindCode] > 0, want[journalMutationKindCode])
+ }
+ }
+}
+
+// FuzzJournal drives a randomised sequence of state mutations, snapshots and
+// reverts against a fresh StateDB and validates the journal's internal
+// bookkeeping invariants after every step. It also asserts that reverting
+// back to the root snapshot empties mutations, originals and entries
+// completely. The seed corpus ensures the test also runs as a regular unit
+// test via `go test -run FuzzJournal`.
+func FuzzJournal(f *testing.F) {
+ seeds := [][]byte{
+ // balance then full revert (simplest a→b→a case).
+ {0x00, 0x00, 0x05, 0x05, 0x00},
+ // balance+nonce+code mixed, then revert to root.
+ {0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x05, 0x00},
+ // snapshot, mutate, revert, mutate again.
+ {0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x01, 0x05},
+ // storage interleaved with metadata.
+ {0x03, 0x00, 0x01, 0x00, 0x01, 0x05, 0x03, 0x02, 0x02, 0x04, 0x03, 0x01, 0x07},
+ // many ops, no explicit revert — exercises steady-state invariants.
+ {0x00, 0x01, 0x02, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02,
+ 0x03, 0x04, 0x00, 0x01, 0x02, 0x00, 0x06, 0x08, 0x0a, 0x0c},
+ }
+ for _, s := range seeds {
+ f.Add(s)
+ }
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ sdb, err := New(types.EmptyRootHash, NewDatabaseForTesting())
+ if err != nil {
+ t.Fatal(err)
+ }
+ root := sdb.Snapshot()
+
+ // Stack of snapshot IDs taken during the fuzz loop.
+ var pending []int
+
+ // readByte returns the next byte and advances the cursor. Returns
+ // (0, false) if exhausted.
+ i := 0
+ readByte := func() (byte, bool) {
+ if i >= len(data) {
+ return 0, false
+ }
+ b := data[i]
+ i++
+ return b, true
+ }
+
+ for {
+ op, ok := readByte()
+ if !ok {
+ break
+ }
+ switch op % 6 {
+ case 0: // SetBalance
+ a, ok1 := readByte()
+ v, ok2 := readByte()
+ if !ok1 || !ok2 {
+ break
+ }
+ addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
+ sdb.SetBalance(addr, uint256.NewInt(uint64(v)), tracing.BalanceChangeUnspecified)
+ case 1: // SetNonce
+ a, ok1 := readByte()
+ n, ok2 := readByte()
+ if !ok1 || !ok2 {
+ break
+ }
+ addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
+ sdb.SetNonce(addr, uint64(n), tracing.NonceChangeUnspecified)
+ case 2: // SetCode
+ a, ok1 := readByte()
+ l, ok2 := readByte()
+ if !ok1 || !ok2 {
+ break
+ }
+ addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
+ code := make([]byte, int(l)%8)
+ for k := range code {
+ b, ok := readByte()
+ if !ok {
+ break
+ }
+ code[k] = b
+ }
+ sdb.SetCode(addr, code, tracing.CodeChangeUnspecified)
+ case 3: // SetState (storage; tracked as mutation kind, no original)
+ a, ok1 := readByte()
+ k, ok2 := readByte()
+ v, ok3 := readByte()
+ if !ok1 || !ok2 || !ok3 {
+ break
+ }
+ addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
+ sdb.SetState(addr,
+ common.BytesToHash([]byte{k}),
+ common.BytesToHash([]byte{v}))
+ case 4: // Snapshot
+ pending = append(pending, sdb.Snapshot())
+ case 5: // RevertToSnapshot
+ if len(pending) == 0 {
+ break
+ }
+ sel, ok := readByte()
+ if !ok {
+ break
+ }
+ idx := int(sel) % len(pending)
+ sdb.RevertToSnapshot(pending[idx])
+ pending = pending[:idx]
+ }
+ checkJournalInvariants(t, sdb.journal)
+ }
+
+ // After reverting to the root snapshot, the journal must be fully
+ // drained: no entries, no mutations, no originals. This is the core
+ // guarantee the user cares about — "all mutations against a single
+ // account reverted" taken to its limit across every account.
+ sdb.RevertToSnapshot(root)
+ checkJournalInvariants(t, sdb.journal)
+
+ if n := len(sdb.journal.entries); n != 0 {
+ t.Fatalf("entries not drained after revert-to-root: %d remain", n)
+ }
+ if n := len(sdb.journal.mutations); n != 0 {
+ t.Fatalf("mutations not drained after revert-to-root: %d remain", n)
+ }
+ })
+}
diff --git a/core/state/reader.go b/core/state/reader.go
index 5df0acbb9b..be07cec0f9 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -255,7 +255,7 @@ type ubtTrieReader struct {
// newUBTTrieReader constructs a Unified-binary-trie reader of the specific state.
// An error will be returned if the associated trie specified by root is not existent.
func newUBTTrieReader(root common.Hash, db *triedb.Database) (*ubtTrieReader, error) {
- binTrie, binErr := bintrie.NewBinaryTrie(root, db)
+ binTrie, binErr := bintrie.NewBinaryTrie(root, db, db.BinTrieGroupDepth())
if binErr != nil {
return nil, binErr
}
diff --git a/core/state/snapshot/iterator_test.go b/core/state/snapshot/iterator_test.go
index a95bd66dde..8e473aa312 100644
--- a/core/state/snapshot/iterator_test.go
+++ b/core/state/snapshot/iterator_test.go
@@ -441,7 +441,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) {
if i%8 == 0 {
e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
}
- if i > 50 || i < 85 {
+ if i > 50 && i < 85 {
f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
}
if i%64 == 0 {
diff --git a/core/state/state_object.go b/core/state/state_object.go
index 8e72486825..ce456e7668 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -184,8 +184,9 @@ func (s *stateObject) getState(key common.Hash) (common.Hash, common.Hash) {
// without any mutations caused in the current execution.
func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
// Record slot access regardless of whether the storage slot exists.
- s.db.stateReadList.AddState(s.address, key)
-
+ if s.db.stateAccessList != nil {
+ s.db.stateAccessList.StorageRead(s.address, key)
+ }
// If we have a pending write or clean cached, return that
if value, pending := s.pendingStorage[key]; pending {
return value
@@ -274,6 +275,13 @@ func (s *stateObject) finalise() {
// map as the dirty slot might have been committed already (before the
// byzantium fork) and entry is necessary to modify the value back.
s.pendingStorage[key] = value
+
+ // Aggregate storage writes into the block-level access list.
+ // All slots in the dirtyStorage set must have post-transaction
+ // values that differ from their pre-transaction values.
+ if s.db.stateAccessList != nil {
+ s.db.stateAccessList.StorageWrite(s.db.blockAccessIndex, s.address, key, value)
+ }
}
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil {
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 1858f4758d..1c49d46020 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -18,6 +18,7 @@
package state
import (
+ "bytes"
"errors"
"fmt"
"maps"
@@ -128,7 +129,10 @@ type StateDB struct {
accessEvents *AccessEvents
// Per-transaction state access footprint for EIP-7928
- stateReadList *bal.StateAccessList
+ stateAccessList *bal.ConstructionBlockAccessList
+
+ // Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution)
+ blockAccessIndex uint32
// Transient storage
transientStorage transientStorage
@@ -589,8 +593,9 @@ func (s *StateDB) deleteStateObject(addr common.Address) {
// the object is not found or was deleted in this execution context.
func (s *StateDB) getStateObject(addr common.Address) *stateObject {
// Record state access regardless of whether the account exists.
- s.stateReadList.AddAccount(addr)
-
+ if s.stateAccessList != nil {
+ s.stateAccessList.AccountRead(addr)
+ }
// Prefer live objects if any is available
if obj := s.stateObjects[addr]; obj != nil {
return obj
@@ -693,6 +698,7 @@ func (s *StateDB) Copy() *StateDB {
refund: s.refund,
thash: s.thash,
txIndex: s.txIndex,
+ blockAccessIndex: s.blockAccessIndex,
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
logSize: s.logSize,
preimages: maps.Clone(s.preimages),
@@ -716,9 +722,6 @@ func (s *StateDB) Copy() *StateDB {
if s.accessEvents != nil {
state.accessEvents = s.accessEvents.Copy()
}
- if s.stateReadList != nil {
- state.stateReadList = s.stateReadList.Copy()
- }
// Deep copy cached state objects.
for addr, obj := range s.stateObjects {
state.stateObjects[addr] = obj.deepCopy(state)
@@ -740,6 +743,9 @@ func (s *StateDB) Copy() *StateDB {
}
state.logs[hash] = cpy
}
+ if s.stateAccessList != nil {
+ state.stateAccessList = s.stateAccessList.Copy()
+ }
return state
}
@@ -775,7 +781,7 @@ type removedAccountWithBalance struct {
// before the Finalise.
func (s *StateDB) LogsForBurnAccounts() []*types.Log {
var list []removedAccountWithBalance
- for addr := range s.journal.dirties {
+ for addr := range s.journal.mutations {
if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() {
list = append(list, removedAccountWithBalance{
address: obj.address,
@@ -799,17 +805,20 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log {
// Finalise finalises the state by removing the destructed objects and clears
// the journal as well as the refunds. Finalise, however, will not push any updates
// into the tries just yet. Only IntermediateRoot or Commit will do that.
-func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
- addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties))
- for addr := range s.journal.dirties {
+func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList {
+ addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations))
+ for addr, state := range s.journal.mutations {
obj, exist := s.stateObjects[addr]
if !exist {
- // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
- // That tx goes out of gas, and although the notion of 'touched' does not exist there, the
- // touch-event will still be recorded in the journal. Since ripeMD is a special snowflake,
- // it will persist in the journal even though the journal is reverted. In this special circumstance,
- // it may exist in `s.journal.dirties` but not in `s.stateObjects`.
- // Thus, we can safely ignore it here
+ // RIPEMD160 (0x03) gets an extra dirty marker for a historical
+ // mainnet consensus exception (at block 1714175, in tx
+ // 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2)
+ // around empty-account touch/revert handling.
+ //
+ // That marker survives journal revert, so the account may remain in
+ // s.journal.mutations even though its state object was rolled
+ // back and no longer exists. In that case there is nothing to
+ // finalise or delete, so ignore it here.
continue
}
if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) {
@@ -822,7 +831,43 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
if _, ok := s.stateObjectsDestruct[obj.address]; !ok {
s.stateObjectsDestruct[obj.address] = obj
}
+ // Aggregate the account mutation into the block-level accessList
+ // if Amsterdam has been activated.
+ if s.stateAccessList != nil {
+ // Notably, if the account is deleted during the transaction,
+ // its pre-transaction nonce, code, and storage must be empty.
+ //
+ // EIP-6780 restricts self-destruct to contracts deployed within
+ // the same transaction, while EIP-7610 rejects deployments to
+ // destinations with non-empty storage, non-zero nonce and non-empty
+ // code.
+ //
+ // Therefore, when an account is deleted, its pre-transaction nonce
+ // code and storage is guaranteed to be empty, leaving nothing to
+ // clean up here.
+ balance := uint256.NewInt(0)
+ if state.balanceSet && balance.Cmp(state.balance) != 0 {
+ s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance)
+ }
+ }
} else {
+ // Aggregate the account mutation into the block-level accessList
+ // if Amsterdam has been activated.
+ if s.stateAccessList != nil {
+ balance := obj.Balance()
+ if state.balanceSet && balance.Cmp(state.balance) != 0 {
+ s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance)
+ }
+ nonce := obj.Nonce()
+ if state.nonceSet && nonce != state.nonce {
+ s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce)
+ }
+ if state.codeSet {
+ if code := obj.Code(); !bytes.Equal(code, state.code) {
+ s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code)
+ }
+ }
+ }
obj.finalise()
s.markUpdate(addr)
}
@@ -839,7 +884,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
// Invalidate journal because reverting across transactions is not allowed.
s.clearJournalAndRefund()
- return s.stateReadList
+ return s.stateAccessList
}
// IntermediateRoot computes the current root hash of the state trie.
@@ -1052,9 +1097,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// SetTxContext sets the current transaction hash and index which are
// used when the EVM emits new state logs. It should be invoked before
// transaction execution.
-func (s *StateDB) SetTxContext(thash common.Hash, ti int) {
+func (s *StateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) {
s.thash = thash
s.txIndex = ti
+ s.blockAccessIndex = blockAccessIndex
}
func (s *StateDB) clearJournalAndRefund() {
@@ -1355,7 +1401,7 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag
// The reader update must be performed as the final step, otherwise,
// the new state would not be visible before db.commit.
- s.reader, _ = s.db.Reader(s.originalRoot)
+ s.reader, err = s.db.Reader(s.originalRoot)
return ret, err
}
@@ -1435,7 +1481,7 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d
s.transientStorage = newTransientStorage()
if rules.IsAmsterdam {
- s.stateReadList = bal.NewStateAccessList()
+ s.stateAccessList = bal.NewConstructionBlockAccessList()
}
}
diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go
index c5faa7c98e..98d01343a4 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -234,7 +234,7 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log {
return s.inner.LogsForBurnAccounts()
}
-func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
+func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList {
if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil {
// Short circuit if no relevant hooks are set.
return s.inner.Finalise(deleteEmptyObjects)
@@ -244,7 +244,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
// that state change hooks will be invoked in deterministic
// order when the accounts are deleted below
var selfDestructedAddrs []common.Address
- for addr := range s.inner.journal.dirties {
+ for addr := range s.inner.journal.mutations {
obj := s.inner.stateObjects[addr]
if obj == nil || !obj.selfDestructed {
// Not self-destructed, keep searching.
@@ -288,3 +288,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList {
}
return s.inner.Finalise(deleteEmptyObjects)
}
+
+func (s *hookedStateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) {
+ s.inner.SetTxContext(thash, ti, blockAccessIndex)
+}
diff --git a/core/state/statedb_hooked_test.go b/core/state/statedb_hooked_test.go
index 6fe17ec1b4..fad234f848 100644
--- a/core/state/statedb_hooked_test.go
+++ b/core/state/statedb_hooked_test.go
@@ -82,7 +82,7 @@ func TestBurn(t *testing.T) {
// TestHooks is a basic sanity-check of all hooks
func TestHooks(t *testing.T) {
inner, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
- inner.SetTxContext(common.Hash{0x11}, 100) // For the log
+ inner.SetTxContext(common.Hash{0x11}, 100, 101) // For the log
var result []string
var wants = []string{
"0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)",
diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go
index b5ef42b3e0..0bf9b50e7b 100644
--- a/core/state/statedb_test.go
+++ b/core/state/statedb_test.go
@@ -662,26 +662,30 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
return fmt.Errorf("got GetLogs(common.Hash{}) == %v, want GetLogs(common.Hash{}) == %v",
state.GetLogs(common.Hash{}, 0, common.Hash{}, 0), checkstate.GetLogs(common.Hash{}, 0, common.Hash{}, 0))
}
- if !maps.Equal(state.journal.dirties, checkstate.journal.dirties) {
- getKeys := func(dirty map[common.Address]int) string {
- var keys []common.Address
- out := new(strings.Builder)
- for key := range dirty {
- keys = append(keys, key)
- }
- slices.SortFunc(keys, common.Address.Cmp)
- for i, key := range keys {
- fmt.Fprintf(out, " %d. %v\n", i, key)
- }
- return out.String()
- }
- have := getKeys(state.journal.dirties)
- want := getKeys(checkstate.journal.dirties)
- return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", have, want)
+ if !equalMutationSets(state.journal.mutations, checkstate.journal.mutations) {
+ return fmt.Errorf("journal mutation set mismatch.\nhave:\n%v\nwant:\n%v\n", state.journal.mutations, checkstate.journal.mutations)
}
return nil
}
+// equalMutationSets checks that two journal mutation maps have the same set of
+// addresses and, for each address, the same per-kind counts. The stashed
+// original values are ignored because comparing them across two independent
+// state databases (with distinct pointer identities) isn't the point of this
+// check — we only care that the two journals agree on what was touched.
+func equalMutationSets(a, b map[common.Address]*journalMutationState) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for addr, sa := range a {
+ sb, ok := b[addr]
+ if !ok || sa.counts != sb.counts {
+ return false
+ }
+ }
+ return true
+}
+
func TestTouchDelete(t *testing.T) {
s := newStateEnv()
s.state.getOrNewStateObject(common.Address{})
@@ -691,12 +695,54 @@ func TestTouchDelete(t *testing.T) {
snapshot := s.state.Snapshot()
s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified)
- if len(s.state.journal.dirties) != 1 {
- t.Fatal("expected one dirty state object")
+ if len(s.state.journal.mutations) != 1 {
+ t.Fatal("expected one mutated state object")
}
s.state.RevertToSnapshot(snapshot)
- if len(s.state.journal.dirties) != 0 {
- t.Fatal("expected no dirty state object")
+ if len(s.state.journal.mutations) != 0 {
+ t.Fatal("expected no journal mutations")
+ }
+}
+
+func TestJournalMutationTracking(t *testing.T) {
+ state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
+ addr := common.HexToAddress("0x01")
+ key := common.HexToHash("0x02")
+
+ if _, ok := state.journal.mutations[addr]; ok {
+ t.Fatal("unexpected initial mutation entry")
+ }
+ snapshot := state.Snapshot()
+
+ state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ state.SetNonce(addr, 2, tracing.NonceChangeUnspecified)
+ state.SetCode(addr, []byte{0x1}, tracing.CodeChangeUnspecified)
+ state.SetState(addr, key, common.Hash{0x3})
+
+ want := journalMutationCounts{
+ journalMutationKindCreate: 1,
+ journalMutationKindBalance: 1,
+ journalMutationKindNonce: 1,
+ journalMutationKindCode: 1,
+ journalMutationKindStorage: 1,
+ }
+ checkCounts := func(got *journalMutationState, label string) {
+ t.Helper()
+ if got == nil {
+ t.Fatalf("%s: missing mutation entry for %x", label, addr)
+ }
+ if got.counts != want {
+ t.Fatalf("%s: counts=%+v, want=%+v", label, got.counts, want)
+ }
+ }
+ checkCounts(state.journal.mutations[addr], "state")
+
+ copy := state.Copy()
+ checkCounts(copy.journal.mutations[addr], "copy")
+
+ state.RevertToSnapshot(snapshot)
+ if _, ok := state.journal.mutations[addr]; ok {
+ t.Fatalf("unexpected mutation entry after revert")
}
}
diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go
index ed292d0beb..d99611ff2c 100644
--- a/core/state_prefetcher.go
+++ b/core/state_prefetcher.go
@@ -104,7 +104,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
// Disable the nonce check
msg.SkipNonceChecks = true
- stateCpy.SetTxContext(tx.Hash(), i)
+ stateCpy.SetTxContext(tx.Hash(), i, uint32(i+1))
// We attempt to apply a transaction. The goal is not to execute
// the transaction successfully, rather to warm up touched data slots.
diff --git a/core/state_processor.go b/core/state_processor.go
index 54ebbd047b..5690a152e7 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -27,11 +27,13 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
+ "github.com/holiman/uint256"
)
// StateProcessor is a basic Processor, which takes care of transitioning
@@ -75,27 +77,20 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
if hooks := cfg.Tracer; hooks != nil {
tracingStateDB = state.NewHookedState(statedb, hooks)
}
-
// Mutate the block and state according to any hard-fork specs
if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(tracingStateDB)
}
var (
- context vm.BlockContext
- signer = types.MakeSigner(config, header.Number, header.Time)
+ context = NewEVMBlockContext(header, p.chain, nil)
+ signer = types.MakeSigner(config, header.Number, header.Time)
+ evm = vm.NewEVM(context, tracingStateDB, config, cfg)
+ blockAccessList = bal.NewConstructionBlockAccessList()
)
-
- // Apply pre-execution system calls.
- context = NewEVMBlockContext(header, p.chain, nil)
- evm := vm.NewEVM(context, tracingStateDB, config, cfg)
defer evm.Release()
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if config.IsPrague(block.Number(), block.Time()) || config.IsUBT(block.Number(), block.Time()) {
- ProcessParentBlockHash(block.ParentHash(), evm)
- }
+ // Run the pre-execution system calls
+ blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()))
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
@@ -103,66 +98,97 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
if err != nil {
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
- statedb.SetTxContext(tx.Hash(), i)
+ statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
_, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM",
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
telemetry.Int64Attribute("tx.index", int64(i)),
)
-
- receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm)
+ receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm)
if err != nil {
spanEnd(&err)
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
+ blockAccessList.Merge(bal)
spanEnd(nil)
}
- requests, err := postExecution(ctx, config, block, allLogs, evm)
+ requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1))
if err != nil {
return nil, err
}
+ blockAccessList.Merge(bal)
- // Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
- p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body())
+ // Finalize the block, applying any consensus engine specific extras
+ // (e.g. block rewards).
+ //
+ // TODO(rjl493456442) integrate it into the PostExecution.
+ p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList)
return &ProcessResult{
Receipts: receipts,
Requests: requests,
Logs: allLogs,
GasUsed: gp.Used(),
+ Bal: blockAccessList,
}, nil
}
-// postExecution processes the post-execution system calls if Prague is enabled.
-func postExecution(ctx context.Context, config *params.ChainConfig, block *types.Block, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) {
+// PreExecution processes pre-execution system calls.
+func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList {
+ _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution")
+ defer spanEnd(nil)
+
+ var blockAccessList *bal.ConstructionBlockAccessList
+ if config.IsAmsterdam(number, time) {
+ blockAccessList = bal.NewConstructionBlockAccessList()
+ }
+ // EIP-4788
+ if beaconRoot != nil {
+ ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList)
+ }
+ // EIP-2935
+ if config.IsPrague(number, time) || config.IsUBT(number, time) {
+ ProcessParentBlockHash(parent, evm, blockAccessList)
+ }
+ return blockAccessList
+}
+
+// PostExecution processes post-execution system calls when Prague is enabled.
+// If Prague is not activated, it returns null requests to differentiate from
+// empty requests.
+func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution")
defer spanEnd(&err)
+ if config.IsAmsterdam(number, time) {
+ blockAccessList = bal.NewConstructionBlockAccessList()
+ }
// Read requests if Prague is enabled.
- if config.IsPrague(block.Number(), block.Time()) {
+ if config.IsPrague(number, time) {
+ rules := config.Rules(number, true, time) // IsMerge is always true
+
requests = [][]byte{}
// EIP-6110
if err := ParseDepositLogs(&requests, allLogs, config); err != nil {
- return requests, fmt.Errorf("failed to parse deposit logs: %w", err)
+ return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err)
}
// EIP-7002
- if err := ProcessWithdrawalQueue(&requests, evm); err != nil {
- return requests, fmt.Errorf("failed to process withdrawal queue: %w", err)
+ if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
+ return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
}
// EIP-7251
- if err := ProcessConsolidationQueue(&requests, evm); err != nil {
- return requests, fmt.Errorf("failed to process consolidation queue: %w", err)
+ if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
+ return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
}
-
- return requests, nil
+ return requests, blockAccessList, nil
}
// ApplyTransactionWithEVM attempts to apply a transaction to the given state database
// and uses the input parameters for its environment similar to ApplyTransaction. However,
// this method takes an already created EVM instance as input.
-func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) {
+func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, bal *bal.ConstructionBlockAccessList, err error) {
if hooks := evm.Config.Tracer; hooks != nil {
if hooks.OnTxStart != nil {
hooks.OnTxStart(evm.GetVMContext(), tx, msg.From)
@@ -174,12 +200,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
// Apply the transaction to the current state (included in the env).
result, err := ApplyMessage(evm, msg, gp)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// Update the state with pending changes.
var root []byte
if evm.ChainConfig().IsByzantium(blockNumber) {
- evm.StateDB.Finalise(true)
+ bal = evm.StateDB.Finalise(true)
} else {
root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes()
}
@@ -188,7 +214,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
if statedb.Database().Type().Is(state.TypeUBT) {
statedb.AccessEvents().Merge(evm.AccessEvents)
}
- return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil
+ return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil
}
// MakeReceipt generates the receipt object for a transaction given its execution result.
@@ -233,10 +259,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b
// and uses the input parameters for its environment. It returns the receipt
// for the transaction and an error if the transaction failed,
// indicating the block was invalid.
-func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) {
+func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) {
msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// Create a new context to be used in the EVM environment
return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm)
@@ -244,7 +270,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *
// ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root
// contract. This method is exported to be used in tests.
-func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
+func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -254,24 +280,26 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.NewInt(0),
+ GasFeeCap: uint256.NewInt(0),
+ GasTipCap: uint256.NewInt(0),
To: ¶ms.BeaconRootsAddress,
Data: beaconRoot[:],
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
+ evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
_, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ blockAccessList.Merge(evm.StateDB.Finalise(true))
}
// ProcessParentBlockHash stores the parent block hash in the history storage contract
// as per EIP-2935/7709.
-func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
+func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -281,13 +309,15 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.NewInt(0),
+ GasFeeCap: uint256.NewInt(0),
+ GasTipCap: uint256.NewInt(0),
To: ¶ms.HistoryStorageAddress,
Data: prevHash.Bytes(),
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
+ evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
_, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if err != nil {
@@ -296,22 +326,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ blockAccessList.Merge(evm.StateDB.Finalise(true))
}
// ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract.
// It returns the opaque request data returned by the contract.
-func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) error {
- return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress)
+func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
+ return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList)
}
// ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract.
// It returns the opaque request data returned by the contract.
-func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) error {
- return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress)
+func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
+ return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList)
}
-func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error {
+func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -321,21 +351,25 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
- GasPrice: common.Big0,
- GasFeeCap: common.Big0,
- GasTipCap: common.Big0,
+ GasPrice: uint256.NewInt(0),
+ GasFeeCap: uint256.NewInt(0),
+ GasTipCap: uint256.NewInt(0),
To: &addr,
}
evm.SetTxContext(NewEVMTxContext(msg))
+ evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil)
+ evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr)
ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
- evm.StateDB.Finalise(true)
+ bal := evm.StateDB.Finalise(true)
if err != nil {
return fmt.Errorf("system call failed to execute: %v", err)
}
+ blockAccessList.Merge(bal)
+
if len(ret) == 0 {
return nil // skip empty output
}
@@ -378,8 +412,16 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) {
// AssembleBlock finalizes the state and assembles the block with provided
// body and receipts.
-func AssembleBlock(engine consensus.Engine, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) *types.Block {
- engine.Finalize(chain, header, state, body)
+func AssembleBlock(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, blockAccessList *bal.ConstructionBlockAccessList) *types.Block {
+ // Assign the post-transition state root
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
- return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
+
+ if !chain.Config().IsAmsterdam(header.Number, header.Time) {
+ return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
+ }
+ // Assign the BlockAccessListHash if Amsterdam has been enabled
+ bal := blockAccessList.ToEncodingObj()
+ balHash := bal.Hash()
+ header.BlockAccessListHash = &balHash
+ return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)).WithAccessListUnsafe(bal)
}
diff --git a/core/state_transition.go b/core/state_transition.go
index c7b0593857..51c5836892 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -68,7 +68,7 @@ func (result *ExecutionResult) Revert() []byte {
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
-func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860 bool) (vm.GasCosts, error) {
+func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) {
// Set the starting gas for the raw transaction
var gas uint64
if isContractCreation && isHomestead {
@@ -107,8 +107,32 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
}
}
if accessList != nil {
- gas += uint64(len(accessList)) * params.TxAccessListAddressGas
- gas += uint64(accessList.StorageKeys()) * params.TxAccessListStorageKeyGas
+ addresses := uint64(len(accessList))
+ storageKeys := uint64(accessList.StorageKeys())
+ if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses {
+ return vm.GasCosts{}, ErrGasUintOverflow
+ }
+ gas += addresses * params.TxAccessListAddressGas
+ if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys {
+ return vm.GasCosts{}, ErrGasUintOverflow
+ }
+ gas += storageKeys * params.TxAccessListStorageKeyGas
+
+ // EIP-7981: access list data is charged in addition to the base charge.
+ if isAmsterdam {
+ const (
+ addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
+ storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
+ )
+ if (math.MaxUint64-gas)/addressCost < addresses {
+ return vm.GasCosts{}, ErrGasUintOverflow
+ }
+ gas += addresses * addressCost
+ if (math.MaxUint64-gas)/storageKeyCost < storageKeys {
+ return vm.GasCosts{}, ErrGasUintOverflow
+ }
+ gas += storageKeys * storageKeyCost
+ }
}
if authList != nil {
gas += uint64(len(authList)) * params.CallNewAccountGas
@@ -117,7 +141,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
}
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
-func FloorDataGas(rules params.Rules, data []byte) (uint64, error) {
+func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) {
var (
tokens uint64
tokenCost uint64
@@ -125,15 +149,41 @@ func FloorDataGas(rules params.Rules, data []byte) (uint64, error) {
if rules.IsAmsterdam {
// EIP-7976 changes how calldata is priced.
// From 10/40 to 64/64 for zero/non-zero bytes.
- tokens = uint64(len(data)) * params.TxTokenPerNonZeroByte
tokenCost = params.TxCostFloorPerToken7976
+ dataLen := uint64(len(data))
+ if math.MaxUint64/params.TxTokenPerNonZeroByte < dataLen {
+ return 0, ErrGasUintOverflow
+ }
+ tokens = dataLen * params.TxTokenPerNonZeroByte
+
+ // EIP-7981 adds additional tokens for every entry in the accesslist
+ const addressTokenCost = uint64(common.AddressLength) * params.TxTokenPerNonZeroByte
+ addresses := uint64(len(accessList))
+ if (math.MaxUint64-tokens)/addressTokenCost < addresses {
+ return 0, ErrGasUintOverflow
+ }
+ tokens += addresses * addressTokenCost
+
+ const storageKeyTokenCost = uint64(common.HashLength) * params.TxTokenPerNonZeroByte
+ storageKeys := uint64(accessList.StorageKeys())
+ if (math.MaxUint64-tokens)/storageKeyTokenCost < storageKeys {
+ return 0, ErrGasUintOverflow
+ }
+ tokens += storageKeys * storageKeyTokenCost
} else {
var (
z = uint64(bytes.Count(data, []byte{0}))
nz = uint64(len(data)) - z
)
// Pre-Amsterdam
- tokens = nz*params.TxTokenPerNonZeroByte + z
+ if math.MaxUint64/params.TxTokenPerNonZeroByte < nz {
+ return 0, ErrGasUintOverflow
+ }
+ tokens = nz * params.TxTokenPerNonZeroByte
+ if math.MaxUint64-tokens < z {
+ return 0, ErrGasUintOverflow
+ }
+ tokens += z
tokenCost = params.TxCostFloorPerToken
}
@@ -160,14 +210,14 @@ type Message struct {
To *common.Address
From common.Address
Nonce uint64
- Value *big.Int
+ Value *uint256.Int
GasLimit uint64
- GasPrice *big.Int
- GasFeeCap *big.Int
- GasTipCap *big.Int
+ GasPrice *uint256.Int
+ GasFeeCap *uint256.Int
+ GasTipCap *uint256.Int
Data []byte
AccessList types.AccessList
- BlobGasFeeCap *big.Int
+ BlobGasFeeCap *uint256.Int
BlobHashes []common.Hash
SetCodeAuthorizations []types.SetCodeAuthorization
@@ -188,32 +238,64 @@ type Message struct {
// TransactionToMessage converts a transaction into a Message.
func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
+ from, err := types.Sender(s, tx)
+ if err != nil {
+ return nil, err
+ }
+ gasPrice, overflow := uint256.FromBig(tx.GasPrice())
+ if overflow {
+ return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
+ from.Hex(), tx.GasPrice().BitLen())
+ }
+ txGasFeeCap := tx.GasFeeCap()
+ gasFeeCap, overflow := uint256.FromBig(txGasFeeCap)
+ if overflow {
+ return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
+ from.Hex(), tx.GasFeeCap().BitLen())
+ }
+ txGasTipCap := tx.GasTipCap()
+ gasTipCap, overflow := uint256.FromBig(txGasTipCap)
+ if overflow {
+ return nil, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
+ from.Hex(), tx.GasTipCap().BitLen())
+ }
+ value, overflow := uint256.FromBig(tx.Value())
+ if overflow {
+ return nil, fmt.Errorf("value exceeds 256 bits: address %v", from.Hex())
+ }
+ blobGasFeeCap, overflow := uint256.FromBig(tx.BlobGasFeeCap())
+ if overflow {
+ return nil, fmt.Errorf("blobGasFeeCap exceeds 256 bits: address %v", from.Hex())
+ }
+
msg := &Message{
+ From: from,
Nonce: tx.Nonce(),
GasLimit: tx.Gas(),
- GasPrice: tx.GasPrice(),
- GasFeeCap: tx.GasFeeCap(),
- GasTipCap: tx.GasTipCap(),
+ GasPrice: gasPrice,
+ GasFeeCap: gasFeeCap,
+ GasTipCap: gasTipCap,
To: tx.To(),
- Value: tx.Value(),
+ Value: value,
Data: tx.Data(),
AccessList: tx.AccessList(),
SetCodeAuthorizations: tx.SetCodeAuthorizations(),
SkipNonceChecks: false,
SkipTransactionChecks: false,
BlobHashes: tx.BlobHashes(),
- BlobGasFeeCap: tx.BlobGasFeeCap(),
+ BlobGasFeeCap: blobGasFeeCap,
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
- msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee)
- if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 {
- msg.GasPrice = msg.GasFeeCap
+ effectiveGasPrice := new(big.Int).Add(baseFee, txGasTipCap)
+ if effectiveGasPrice.Cmp(txGasFeeCap) > 0 {
+ effectiveGasPrice = txGasFeeCap
}
+ // EffectiveGasPrice is already capped by txGasFeeCap, therefore
+ // the overflow check is not required.
+ msg.GasPrice = uint256.MustFromBig(effectiveGasPrice)
}
- var err error
- msg.From, err = types.Sender(s, tx)
- return msg, err
+ return msg, nil
}
// ApplyMessage computes the new state by applying the given message
@@ -283,46 +365,70 @@ func (st *stateTransition) to() common.Address {
}
func (st *stateTransition) buyGas() error {
- mgval := new(big.Int).SetUint64(st.msg.GasLimit)
- mgval.Mul(mgval, st.msg.GasPrice)
- balanceCheck := new(big.Int).Set(mgval)
+ mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
+ _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice)
+ if overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
+ balanceCheck := new(uint256.Int).Set(mgval)
if st.msg.GasFeeCap != nil {
balanceCheck.SetUint64(st.msg.GasLimit)
- balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
+ if _, overflow := balanceCheck.MulOverflow(balanceCheck, st.msg.GasFeeCap); overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
+ }
+ if st.msg.Value != nil {
+ if _, overflow := balanceCheck.AddOverflow(balanceCheck, st.msg.Value); overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
}
- balanceCheck.Add(balanceCheck, st.msg.Value)
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if blobGas := st.blobGasUsed(); blobGas > 0 {
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
- blobBalanceCheck := new(big.Int).SetUint64(blobGas)
- blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap)
- balanceCheck.Add(balanceCheck, blobBalanceCheck)
+ blobBalanceCheck := new(uint256.Int).SetUint64(blobGas)
+ if _, overflow := blobBalanceCheck.MulOverflow(blobBalanceCheck, st.msg.BlobGasFeeCap); overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
+ if _, overflow := balanceCheck.AddOverflow(balanceCheck, blobBalanceCheck); overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
// Pay for blobGasUsed * actual blob fee
- blobFee := new(big.Int).SetUint64(blobGas)
- blobFee.Mul(blobFee, st.evm.Context.BlobBaseFee)
- mgval.Add(mgval, blobFee)
+ blobBaseFee, overflow := uint256.FromBig(st.evm.Context.BlobBaseFee)
+ if overflow {
+ return fmt.Errorf("invalid blobBaseFee: %v", st.evm.Context.BlobBaseFee)
+ }
+ blobFee := new(uint256.Int).SetUint64(blobGas)
+
+ // In practice, overflow checking is unnecessary, as blobBaseFee cannot exceed
+ // BlobGasFeeCap. However, in eth_call it is still possible for users to specify
+ // an excessively large blob base fee and bypass the blob base fee validation.
+ _, overflow = blobFee.MulOverflow(blobFee, blobBaseFee)
+ if overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
+ _, overflow = mgval.AddOverflow(mgval, blobFee)
+ if overflow {
+ return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
+ }
}
}
- balanceCheckU256, overflow := uint256.FromBig(balanceCheck)
- if overflow {
- return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
- }
- if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; 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)
}
if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
return err
}
- if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil {
- st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance)
+ if st.evm.Config.Tracer.HasGasHook() {
+ empty := vm.GasBudget{}
+ initial := vm.NewGasBudget(st.msg.GasLimit)
+ st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance)
}
st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit)
st.initialBudget = st.gasRemaining.Copy()
- mgvalU256, _ := uint256.FromBig(mgval)
- st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy)
+ st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy)
return nil
}
@@ -362,21 +468,13 @@ func (st *stateTransition) preCheck() error {
// Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call)
skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0
if !skipCheck {
- if l := msg.GasFeeCap.BitLen(); l > 256 {
- return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
- msg.From.Hex(), l)
- }
- if l := msg.GasTipCap.BitLen(); l > 256 {
- return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
- msg.From.Hex(), l)
- }
if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap,
msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap)
}
// This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation.
- if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
+ if msg.GasFeeCap.CmpBig(st.evm.Context.BaseFee) < 0 {
return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow,
msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee)
}
@@ -410,7 +508,7 @@ func (st *stateTransition) preCheck() error {
if !skipCheck {
// This will panic if blobBaseFee is nil, but blobBaseFee presence
// is verified as part of header validation.
- if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 {
+ if msg.BlobGasFeeCap.CmpBig(st.evm.Context.BlobBaseFee) < 0 {
return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow,
msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee)
}
@@ -462,7 +560,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
floorDataGas uint64
)
// Check clauses 4-5, subtract intrinsic gas if everything is correct
- cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
+ cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
if err != nil {
return nil, err
}
@@ -470,12 +568,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if !sufficient {
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas)
}
- if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
- t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas)
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas)
}
// Gas limit suffices for the floor data cost (EIP-7623)
if rules.IsPrague {
- floorDataGas, err = FloorDataGas(rules, msg.Data)
+ floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList)
if err != nil {
return nil, err
}
@@ -493,9 +591,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// Check clause 6
- value, overflow := uint256.FromBig(msg.Value)
- if overflow {
- return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
+ value := msg.Value
+ if value == nil {
+ value = new(uint256.Int)
}
if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, value) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
@@ -510,7 +608,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
- // - reset transient storage(eip 1153)
+ // - reset transient storage(EIP-1153)
+ // - enable block-level accessList construction (EIP-7928)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
var (
@@ -555,8 +654,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// After EIP-7623: Data-heavy transactions pay the floor gas.
if used := st.gasUsed(); used < floorDataGas {
prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used})
- if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
- t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor)
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor)
}
}
if peakGasUsed < floorDataGas {
@@ -579,9 +678,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
effectiveTip := msg.GasPrice
if rules.IsLondon {
- effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee)
+ baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee)
+ if overflow {
+ return nil, fmt.Errorf("invalid baseFee: %v", st.evm.Context.BaseFee)
+ }
+ effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee)
}
- effectiveTipU256, _ := uint256.FromBig(effectiveTip)
if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
// Skip fee payment when NoBaseFee is set and the fee fields
@@ -589,7 +691,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// the coinbase when simulating calls.
} else {
fee := new(uint256.Int).SetUint64(st.gasUsed())
- fee.Mul(fee, effectiveTipU256)
+ fee.Mul(fee, effectiveTip)
st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee)
// add the coinbase to the witness iff the fee is greater than 0
@@ -681,8 +783,11 @@ func (st *stateTransition) calcRefund() vm.GasBudget {
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
- if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 {
- st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, st.gasRemaining.RegularGas+refund, tracing.GasChangeTxRefunds)
+ if refund > 0 && st.evm.Config.Tracer.HasGasHook() {
+ after := st.gasRemaining
+ after.RegularGas += refund
+
+ st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds)
}
return vm.NewGasBudget(refund)
}
@@ -691,11 +796,13 @@ func (st *stateTransition) calcRefund() vm.GasBudget {
// exchanged at the original rate.
func (st *stateTransition) returnGas() {
remaining := uint256.NewInt(st.gasRemaining.RegularGas)
- remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
+ remaining.Mul(remaining, st.msg.GasPrice)
st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
- if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 {
- st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, 0, tracing.GasChangeTxLeftOverReturned)
+ if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() {
+ after := st.gasRemaining
+ after.RegularGas = 0
+ st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned)
}
}
diff --git a/core/state_transition_test.go b/core/state_transition_test.go
new file mode 100644
index 0000000000..8aab016123
--- /dev/null
+++ b/core/state_transition_test.go
@@ -0,0 +1,287 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+func TestFloorDataGas(t *testing.T) {
+ addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
+ addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222")
+ key1 := common.HexToHash("0xaa")
+ key2 := common.HexToHash("0xbb")
+
+ tests := []struct {
+ name string
+ amsterdam bool
+ data []byte
+ accessList types.AccessList
+ want uint64
+ }{
+ {
+ name: "pre-amsterdam/empty",
+ want: params.TxGas,
+ },
+ {
+ name: "pre-amsterdam/zero-bytes-only",
+ data: bytes.Repeat([]byte{0x00}, 100),
+ // 100 zero tokens * 10 cost = 1000
+ want: params.TxGas + 100*params.TxCostFloorPerToken,
+ },
+ {
+ name: "pre-amsterdam/non-zero-bytes-only",
+ data: bytes.Repeat([]byte{0xff}, 100),
+ // 100 nz * 4 tokens * 10 cost = 4000
+ want: params.TxGas + 100*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken,
+ },
+ {
+ name: "pre-amsterdam/mixed",
+ data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
+ // 50 zero + 50*4 nz = 250 tokens * 10 = 2500
+ want: params.TxGas + (50+50*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken,
+ },
+ {
+ name: "pre-amsterdam/access-list-ignored",
+ data: bytes.Repeat([]byte{0xff}, 10),
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1, key2}},
+ },
+ // pre-amsterdam: floor calculation does not include access list
+ want: params.TxGas + 10*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken,
+ },
+ {
+ name: "amsterdam/empty",
+ amsterdam: true,
+ want: params.TxGas,
+ },
+ {
+ name: "amsterdam/data-only",
+ amsterdam: true,
+ data: bytes.Repeat([]byte{0x00}, 1024),
+ // post-amsterdam: every byte = 4 tokens regardless of value
+ want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976,
+ },
+ {
+ name: "amsterdam/data-non-zero",
+ amsterdam: true,
+ data: bytes.Repeat([]byte{0xff}, 1024),
+ // same as zero data post-amsterdam
+ want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976,
+ },
+ {
+ name: "amsterdam/access-list-addresses-only",
+ amsterdam: true,
+ accessList: types.AccessList{
+ {Address: addr1},
+ {Address: addr2},
+ },
+ // 2 * 20 bytes * 4 tokens/byte * 16 cost/token
+ want: params.TxGas + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976,
+ },
+ {
+ name: "amsterdam/access-list-with-storage-keys",
+ amsterdam: true,
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1, key2}},
+ },
+ // 1 addr * 20 * 4 + 2 keys * 32 * 4 = 80 + 256 = 336 tokens * 16
+ want: params.TxGas + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976,
+ },
+ {
+ name: "amsterdam/mixed",
+ amsterdam: true,
+ data: bytes.Repeat([]byte{0xff}, 100),
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1}},
+ {Address: addr2, StorageKeys: []common.Hash{key1, key2}},
+ },
+ // data: 100*4 = 400; addrs: 2*20*4 = 160; keys: 3*32*4 = 384; total = 944 * 16
+ want: params.TxGas + (100*params.TxTokenPerNonZeroByte+2*common.AddressLength*params.TxTokenPerNonZeroByte+3*common.HashLength*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken7976,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rules := params.Rules{IsAmsterdam: tt.amsterdam}
+ got, err := FloorDataGas(rules, tt.data, tt.accessList)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != tt.want {
+ t.Fatalf("gas mismatch: got %d, want %d", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestIntrinsicGas(t *testing.T) {
+ addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
+ addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222")
+ key1 := common.HexToHash("0xaa")
+ key2 := common.HexToHash("0xbb")
+
+ const (
+ amsterdamAddressCost = uint64(common.AddressLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 1280
+ amsterdamStorageKeyCost = uint64(common.HashLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 2048
+ )
+
+ tests := []struct {
+ name string
+ data []byte
+ accessList types.AccessList
+ authList []types.SetCodeAuthorization
+ creation bool
+ isHomestead bool
+ isEIP2028 bool
+ isEIP3860 bool
+ isAmsterdam bool
+ want uint64
+ }{
+ {
+ name: "frontier/empty-call",
+ want: params.TxGas,
+ },
+ {
+ name: "frontier/contract-creation-pre-homestead",
+ creation: true,
+ isHomestead: false,
+ // pre-homestead, contract creation still uses TxGas
+ want: params.TxGas,
+ },
+ {
+ name: "homestead/contract-creation",
+ creation: true,
+ isHomestead: true,
+ want: params.TxGasContractCreation,
+ },
+ {
+ name: "frontier/non-zero-data",
+ data: bytes.Repeat([]byte{0xff}, 100),
+ // 100 nz bytes * 68 (frontier)
+ want: params.TxGas + 100*params.TxDataNonZeroGasFrontier,
+ },
+ {
+ name: "istanbul/non-zero-data",
+ data: bytes.Repeat([]byte{0xff}, 100),
+ isEIP2028: true,
+ // 100 nz bytes * 16 (post-EIP2028)
+ want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028,
+ },
+ {
+ name: "istanbul/zero-data",
+ data: bytes.Repeat([]byte{0x00}, 100),
+ isEIP2028: true,
+ // 100 zero bytes * 4
+ want: params.TxGas + 100*params.TxDataZeroGas,
+ },
+ {
+ name: "istanbul/mixed-data",
+ data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
+ isEIP2028: true,
+ want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028,
+ },
+ {
+ name: "shanghai/init-code-word-gas",
+ data: bytes.Repeat([]byte{0x00}, 64), // 2 words
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true,
+ // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2
+ want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ },
+ {
+ name: "shanghai/init-code-non-multiple-of-32",
+ data: bytes.Repeat([]byte{0x00}, 33), // 2 words (rounded up)
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true,
+ want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ },
+ {
+ name: "berlin/access-list",
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1, key2}},
+ {Address: addr2, StorageKeys: []common.Hash{key1}},
+ },
+ isEIP2028: true,
+ // 2 addrs * 2400 + 3 keys * 1900
+ want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas,
+ },
+ {
+ name: "amsterdam/access-list-extra-cost",
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1, key2}},
+ {Address: addr2, StorageKeys: []common.Hash{key1}},
+ },
+ isEIP2028: true,
+ isAmsterdam: true,
+ // base access-list charge + EIP-7981 extra
+ want: params.TxGas +
+ 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas +
+ 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost,
+ },
+ {
+ name: "prague/auth-list",
+ authList: []types.SetCodeAuthorization{
+ {Address: addr1},
+ {Address: addr2},
+ {Address: addr1},
+ },
+ isEIP2028: true,
+ // 3 auths * 25000
+ want: params.TxGas + 3*params.CallNewAccountGas,
+ },
+ {
+ name: "amsterdam/combined",
+ data: bytes.Repeat([]byte{0xff}, 100),
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1}},
+ },
+ authList: []types.SetCodeAuthorization{
+ {Address: addr2},
+ },
+ isEIP2028: true,
+ isAmsterdam: true,
+ want: params.TxGas +
+ 100*params.TxDataNonZeroGasEIP2028 +
+ 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
+ 1*params.CallNewAccountGas,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList,
+ tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := vm.GasCosts{RegularGas: tt.want}
+ if got != want {
+ t.Fatalf("gas mismatch: got %+v, want %+v", got, want)
+ }
+ })
+ }
+}
diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go
index 62c62ac3b3..6ea3f7ebbf 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -164,10 +164,36 @@ type (
// FaultHook is invoked when an error occurs during the execution of an opcode.
FaultHook = func(pc uint64, op byte, gas, cost uint64, scope OpContext, depth int, err error)
- // GasChangeHook is invoked when the gas changes.
+ // GasChangeHook reports changes to the regular execution gas. Tracers
+ // that don't need visibility into the state-access gas dimension
+ // introduced by EIP-8037 (Amsterdam) can implement only this hook; it
+ // will continue to fire across the Amsterdam fork unchanged.
+ //
+ // If both this hook and GasChangeHookV2 are implemented on the same
+ // tracer, only V2 will be invoked. Implement exactly one to avoid
+ // double-counting.
GasChangeHook = func(old, new uint64, reason GasChangeReason)
- // TODO(sina, rjl), please add GasChangeV2Hook by landing the multi-dimensional gas
+ // GasChangeHookV2 is invoked when any gas dimension changes. It is the
+ // multi-dimensional successor to GasChangeHook, exposing the state-access
+ // gas dimension introduced by EIP-8037 (Amsterdam) alongside the regular
+ // dimension.
+ //
+ // Compatibility:
+ // - Post-Amsterdam: fires for changes to either the regular or the
+ // state-access dimension. The non-changing dimension is passed through
+ // unchanged in both `old` and `new` so consumers always observe the
+ // complete gas vector.
+ // - Pre-Amsterdam: no state-access gas events occur, so the State field
+ // of both `old` and `new` is always zero. Tracers that register only
+ // V2 still receive every regular-gas change as Gas{State: 0} and
+ // behave identically to a V1 tracer; there is no pre-Amsterdam event
+ // a V2-only tracer misses.
+ //
+ // V1 and V2 coexist: when both are registered on a tracer, only V2 is
+ // invoked. Tracers SHOULD register at most one of the two to avoid
+ // double-counting.
+ GasChangeHookV2 = func(old, new Gas, reason GasChangeReason)
/*
- Chain events -
@@ -250,13 +276,14 @@ type (
type Hooks struct {
// VM events
- OnTxStart TxStartHook
- OnTxEnd TxEndHook
- OnEnter EnterHook
- OnExit ExitHook
- OnOpcode OpcodeHook
- OnFault FaultHook
- OnGasChange GasChangeHook
+ OnTxStart TxStartHook
+ OnTxEnd TxEndHook
+ OnEnter EnterHook
+ OnExit ExitHook
+ OnOpcode OpcodeHook
+ OnFault FaultHook
+ OnGasChange GasChangeHook
+ OnGasChangeV2 GasChangeHookV2
// Chain events
OnBlockchainInit BlockchainInitHook
OnClose CloseHook
@@ -280,6 +307,35 @@ type Hooks struct {
OnBlockHashRead BlockHashReadHook
}
+// HasGasHook reports whether any gas-change hook is registered. Call sites
+// should use this to short-circuit before constructing the Gas / GasBudget
+// arguments to EmitGasChange when tracing is off — the dispatch is otherwise
+// always paid the cost of evaluating those args.
+func (h *Hooks) HasGasHook() bool {
+ return h != nil && (h.OnGasChangeV2 != nil || h.OnGasChange != nil)
+}
+
+// EmitGasChange dispatches a gas change event to the registered hooks. If the
+// multi-dimensional OnGasChangeV2 hook is set it is invoked with the full Gas
+// vectors; otherwise the single-dimensional OnGasChange hook is invoked with
+// the regular-gas dimension only. The call is a no-op when the receiver is
+// nil, when neither hook is registered, or when the reason is GasChangeIgnored.
+//
+// Call sites SHOULD use this helper instead of invoking the hooks directly so
+// that both variants stay consistent across the Amsterdam fork boundary.
+func (h *Hooks) EmitGasChange(old, new Gas, reason GasChangeReason) {
+ if h == nil || reason == GasChangeIgnored {
+ return
+ }
+ if h.OnGasChangeV2 != nil {
+ h.OnGasChangeV2(old, new, reason)
+ return
+ }
+ if h.OnGasChange != nil {
+ h.OnGasChange(old.Regular, new.Regular, reason)
+ }
+}
+
// BalanceChangeReason is used to indicate the reason for a balance change, useful
// for tracing and reporting.
type BalanceChangeReason byte
@@ -335,6 +391,19 @@ const (
BalanceChangeRevert BalanceChangeReason = 15
)
+// Gas represents a multi-dimensional gas budget introduced by EIP-8037.
+// It carries the regular execution gas and the state-access gas, which are
+// metered independently from the Amsterdam fork onwards.
+//
+// Before Amsterdam, gas metering is single-dimensional and only the Regular
+// field is meaningful; State is always zero. The struct is shaped so that
+// pre-Amsterdam call sites can populate it as Gas{Regular: g} without loss
+// of fidelity relative to the legacy single-uint64 hook.
+type Gas struct {
+ Regular uint64 // Regular is the budget for ordinary execution gas.
+ State uint64 // State is the budget dedicated to state-access gas (zero pre-Amsterdam).
+}
+
// GasChangeReason is used to indicate the reason for a gas change, useful
// for tracing and reporting.
//
diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go
index 4030a0c339..d33629365f 100644
--- a/core/txpool/blobpool/blobpool.go
+++ b/core/txpool/blobpool/blobpool.go
@@ -116,6 +116,8 @@ const (
announceThreshold = -1
)
+var errLegacyTx = errors.New("legacy transaction format")
+
// blobTxMeta is the minimal subset of types.BlobTx necessary to validate and
// schedule the blob transactions into the following blocks. Only ever add the
// bare minimum needed fields to keep the size down (and thus number of entries
@@ -147,28 +149,137 @@ type blobTxMeta struct {
evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces
}
-// newBlobTxMeta retrieves the indexed metadata fields from a blob transaction
-// and assembles a helper struct to track in memory.
-// Requires the transaction to have a sidecar (or that we introduce a special version tag for no-sidecar).
-func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transaction) *blobTxMeta {
- if tx.BlobTxSidecar() == nil {
- // This should never happen, as the pool only admits blob transactions with a sidecar
+// blobTxForPool is the storage representation of a blob transaction in the
+// blobpool.
+type blobTxForPool struct {
+ Tx *types.Transaction // tx without sidecar
+ Version byte
+ Commitments []kzg4844.Commitment
+ Proofs []kzg4844.Proof
+ Blobs []kzg4844.Blob
+}
+
+// Sidecar returns BlobTxSidecar of ptx.
+func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar {
+ return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs)
+}
+
+// ApplySidecar copies the sidecar's fields into the flat fields.
+func (ptx *blobTxForPool) ApplySidecar(sc *types.BlobTxSidecar) {
+ ptx.Version = sc.Version
+ ptx.Commitments = sc.Commitments
+ ptx.Proofs = sc.Proofs
+ ptx.Blobs = sc.Blobs
+}
+
+// TxSize returns the transaction size on the network without
+// reconstructing the transaction.
+func (ptx *blobTxForPool) TxSize() uint64 {
+ var blobs, commitments, proofs uint64
+ for i := range ptx.Blobs {
+ blobs += rlp.BytesSize(ptx.Blobs[i][:])
+ }
+ for i := range ptx.Commitments {
+ commitments += rlp.BytesSize(ptx.Commitments[i][:])
+ }
+ for i := range ptx.Proofs {
+ proofs += rlp.BytesSize(ptx.Proofs[i][:])
+ }
+ return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs))
+}
+
+// ToTx reconstructs a full Transaction with the sidecar attached.
+func (ptx *blobTxForPool) ToTx() *types.Transaction {
+ return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar())
+}
+
+// newBlobTxForPool decomposes a blob transaction into blobTxForPool type.
+func newBlobTxForPool(tx *types.Transaction) *blobTxForPool {
+ sc := tx.BlobTxSidecar()
+ if sc == nil {
panic("missing blob tx sidecar")
}
+ return &blobTxForPool{
+ Tx: tx.WithoutBlobTxSidecar(),
+ Version: sc.Version,
+ Commitments: sc.Commitments,
+ Proofs: sc.Proofs,
+ Blobs: sc.Blobs,
+ }
+}
+
+// encodeForNetwork transforms stored blobTxForPool RLP into the standard
+// network transaction encoding. This is used for getRLP.
+//
+// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]]
+// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]])
+// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]])
+func encodeForNetwork(storedRLP []byte) ([]byte, error) {
+ elems, err := rlp.SplitListValues(storedRLP)
+ if err != nil {
+ return nil, fmt.Errorf("invalid blobTxForPool RLP: %w", err)
+ }
+ if len(elems) < 5 {
+ return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems))
+ }
+
+ // 1. Extract tx byte and other tx fields
+ txBytes, _, err := rlp.SplitString(elems[0])
+ if err != nil {
+ return nil, fmt.Errorf("invalid tx bytes: %w", err)
+ }
+ if len(txBytes) < 2 {
+ return nil, errors.New("tx bytes too short")
+ }
+ typeByte := txBytes[0]
+ txRLP := txBytes[1:]
+
+ // 2. Find the version of sidecar.
+ version, _, err := rlp.SplitUint64(elems[1])
+ if err != nil || version > 255 {
+ return nil, fmt.Errorf("invalid version: %w", err)
+ }
+ versionByte := byte(version)
+ // 3. Extract sidecar elements.
+ commitmentsRLP := elems[2]
+ proofsRLP := elems[3]
+ blobsRLP := elems[4]
+
+ // 4. Reconstruct into the network format.
+ var outer [][]byte
+ if versionByte == types.BlobSidecarVersion0 {
+ outer = [][]byte{txRLP, blobsRLP, commitmentsRLP, proofsRLP}
+ } else {
+ outer = [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP}
+ }
+ body, err := rlp.MergeListValues(outer)
+ if err != nil {
+ return nil, err
+ }
+ // Prepend type byte and wrap as an RLP string.
+ inner := make([]byte, 1+len(body))
+ inner[0] = typeByte
+ copy(inner[1:], body)
+ return rlp.EncodeToBytes(inner)
+}
+
+// newBlobTxMeta retrieves the indexed metadata fields from a pooled blob
+// transaction and assembles a helper struct to track in memory.
+func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta {
meta := &blobTxMeta{
- hash: tx.Hash(),
- vhashes: tx.BlobHashes(),
- version: tx.BlobTxSidecar().Version,
+ hash: ptx.Tx.Hash(),
+ vhashes: ptx.Tx.BlobHashes(),
+ version: ptx.Version,
id: id,
storageSize: storageSize,
size: size,
- nonce: tx.Nonce(),
- costCap: uint256.MustFromBig(tx.Cost()),
- execTipCap: uint256.MustFromBig(tx.GasTipCap()),
- execFeeCap: uint256.MustFromBig(tx.GasFeeCap()),
- blobFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap()),
- execGas: tx.Gas(),
- blobGas: tx.BlobGas(),
+ nonce: ptx.Tx.Nonce(),
+ costCap: uint256.MustFromBig(ptx.Tx.Cost()),
+ execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()),
+ execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()),
+ blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()),
+ execGas: ptx.Tx.Gas(),
+ blobGas: ptx.Tx.BlobGas(),
}
meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap)
meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap)
@@ -460,10 +571,17 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
return err
}
// Index all transactions on disk and delete anything unprocessable
- var fails []uint64
+ var (
+ toDelete []uint64
+ convertTxs []uint64
+ )
index := func(id uint64, size uint32, blob []byte) {
- if p.parseTransaction(id, size, blob) != nil {
- fails = append(fails, id)
+ err := p.parseTransaction(id, size, blob)
+ if err != nil {
+ toDelete = append(toDelete, id)
+ }
+ if errors.Is(err, errLegacyTx) {
+ convertTxs = append(convertTxs, id)
}
}
store, err := billy.Open(billy.Options{Path: queuedir, Repair: true}, slotter, index)
@@ -472,17 +590,58 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
}
p.store = store
- if len(fails) > 0 {
- log.Warn("Dropping invalidated blob transactions", "ids", fails)
- dropInvalidMeter.Mark(int64(len(fails)))
+ // Migrate legacy transactions (types.Transaction) to pooledBlobTx format.
+ if len(convertTxs) > 0 {
+ for _, id := range convertTxs {
+ var tx types.Transaction
+ data, err := p.store.Get(id)
+ if err != nil {
+ continue
+ }
+ err = rlp.DecodeBytes(data, &tx)
+ if err != nil {
+ continue
+ }
+ if tx.BlobTxSidecar() == nil {
+ continue
+ }
+ ptx := newBlobTxForPool(&tx)
+ blob, err := rlp.EncodeToBytes(ptx)
+ if err != nil {
+ continue
+ }
+ id, err := p.store.Put(blob)
+ if err != nil {
+ continue
+ }
+ meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx)
- for _, id := range fails {
+ // If the newly inserted transaction fails to be tracked,
+ // it should also be removed with those in `toDelete`
+ sender, err := types.Sender(p.signer, ptx.Tx)
+ if err != nil {
+ toDelete = append(toDelete, id)
+ continue
+ }
+ if err := p.trackTransaction(meta, sender); err != nil {
+ toDelete = append(toDelete, id)
+ continue
+ }
+ }
+ }
+
+ if len(toDelete) > 0 {
+ log.Warn("Dropping invalidated blob transactions", "ids", toDelete)
+ dropInvalidMeter.Mark(int64(len(toDelete)))
+
+ for _, id := range toDelete {
if err := p.store.Delete(id); err != nil {
p.Close()
return err
}
}
}
+
// Sort the indexed transactions by nonce and delete anything gapped, create
// the eviction heap of anyone still standing
for addr := range p.index {
@@ -558,36 +717,33 @@ func (p *BlobPool) Close() error {
// parseTransaction is a callback method on pool creation that gets called for
// each transaction on disk to create the in-memory metadata index.
-// Announced state is not initialized here, it needs to be iniitalized seprately.
+// Return value `bool` is set to true when the entry has old Transaction type.
func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error {
- tx := new(types.Transaction)
- if err := rlp.DecodeBytes(blob, tx); err != nil {
- // This path is impossible unless the disk data representation changes
- // across restarts. For that ever improbable case, recover gracefully
- // by ignoring this data entry.
- log.Error("Failed to decode blob pool entry", "id", id, "err", err)
+ var ptx blobTxForPool
+ if err := rlp.DecodeBytes(blob, &ptx); err != nil {
+ kind, content, _, splitErr := rlp.Split(blob)
+ // check whether it is legacy tx type
+ if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 {
+ return errLegacyTx
+ }
return err
}
- if tx.BlobTxSidecar() == nil {
- log.Error("Missing sidecar in blob pool entry", "id", id, "hash", tx.Hash())
- return errors.New("missing blob sidecar")
+ meta := newBlobTxMeta(id, ptx.TxSize(), size, &ptx)
+ sender, err := types.Sender(p.signer, ptx.Tx)
+ if err != nil {
+ return err
}
+ return p.trackTransaction(meta, sender)
+}
- meta := newBlobTxMeta(id, tx.Size(), size, tx)
+// trackTransaction registers a transaction's metadata in the pool's indices.
+func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error {
if p.lookup.exists(meta.hash) {
// This path is only possible after a crash, where deleted items are not
// removed via the normal shutdown-startup procedure and thus may get
// partially resurrected.
- log.Error("Rejecting duplicate blob pool entry", "id", id, "hash", tx.Hash())
- return errors.New("duplicate blob entry")
- }
- sender, err := types.Sender(p.signer, tx)
- if err != nil {
- // This path is impossible unless the signature validity changes across
- // restarts. For that ever improbable case, recover gracefully by ignoring
- // this data entry.
- log.Error("Failed to recover blob tx sender", "id", id, "hash", tx.Hash(), "err", err)
- return err
+ log.Error("Rejecting duplicate blob pool entry", "id", meta.id, "hash", meta.hash)
+ return fmt.Errorf("duplicate blob entry %d, %s", meta.id, meta.hash)
}
if _, ok := p.index[sender]; !ok {
if err := p.reserver.Hold(sender); err != nil {
@@ -863,17 +1019,17 @@ func (p *BlobPool) offload(addr common.Address, nonce uint64, id uint64, inclusi
log.Error("Blobs missing for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err)
return
}
- var tx types.Transaction
- if err = rlp.DecodeBytes(data, &tx); err != nil {
+ var ptx blobTxForPool
+ if err := rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err)
return
}
- block, ok := inclusions[tx.Hash()]
+ block, ok := inclusions[ptx.Tx.Hash()]
if !ok {
log.Warn("Blob transaction swapped out by signer", "from", addr, "nonce", nonce, "id", id)
return
}
- if err := p.limbo.push(&tx, block); err != nil {
+ if err := p.limbo.push(&ptx, block); err != nil {
log.Warn("Failed to offload blob tx into limbo", "err", err)
return
}
@@ -951,13 +1107,13 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err)
continue
}
- var tx types.Transaction
- if err = rlp.DecodeBytes(data, &tx); err != nil {
+ var ptx blobTxForPool
+ if err = rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err)
continue
}
- announcable = append(announcable, tx.WithoutBlobTxSidecar())
- log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash())
+ announcable = append(announcable, ptx.Tx)
+ log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", ptx.Tx.Hash())
}
}
}
@@ -1108,7 +1264,7 @@ func (p *BlobPool) reorg(oldHead, newHead *types.Header) (map[common.Address][]*
func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
// Retrieve the associated blob from the limbo. Without the blobs, we cannot
// add the transaction back into the pool as it is not mineable.
- tx, err := p.limbo.pull(txhash)
+ ptx, err := p.limbo.pull(txhash)
if err != nil {
log.Error("Blobs unavailable, dropping reorged tx", "err", err)
return err
@@ -1124,30 +1280,29 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
// could theoretically halt a Geth node for ~1.2s by reorging per block. However,
// this attack is financially inefficient to execute.
head := p.head.Load()
- if p.chain.Config().IsOsaka(head.Number, head.Time) && tx.BlobTxSidecar().Version == types.BlobSidecarVersion0 {
- if err := tx.BlobTxSidecar().ToV1(); err != nil {
+ if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.Version == types.BlobSidecarVersion0 {
+ sc := ptx.Sidecar()
+ if err := sc.ToV1(); err != nil {
log.Error("Failed to convert the legacy sidecar", "err", err)
return err
}
- log.Info("Legacy blob transaction is reorged", "hash", tx.Hash())
+ ptx.ApplySidecar(sc)
+ log.Info("Legacy blob transaction is reorged", "hash", ptx.Tx.Hash())
}
- // Serialize the transaction back into the primary datastore.
- blob, err := rlp.EncodeToBytes(tx)
+ blob, err := rlp.EncodeToBytes(ptx)
if err != nil {
- log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err)
+ log.Error("Failed to encode transaction for storage", "hash", ptx.Tx.Hash(), "err", err)
return err
}
id, err := p.store.Put(blob)
if err != nil {
- log.Error("Failed to write transaction into storage", "hash", tx.Hash(), "err", err)
+ log.Error("Failed to write transaction into storage", "hash", ptx.Tx.Hash(), "err", err)
return err
}
-
- // Update the indices and metrics
- meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx)
+ meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx)
if _, ok := p.index[addr]; !ok {
if err := p.reserver.Hold(addr); err != nil {
- log.Warn("Failed to reserve account for blob pool", "tx", tx.Hash(), "from", addr, "err", err)
+ log.Warn("Failed to reserve account for blob pool", "tx", ptx.Tx.Hash(), "from", addr, "err", err)
return err
}
p.index[addr] = []*blobTxMeta{meta}
@@ -1404,20 +1559,32 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction {
if len(data) == 0 {
return nil
}
- item := new(types.Transaction)
- if err := rlp.DecodeBytes(data, item); err != nil {
+ var ptx blobTxForPool
+ if err := rlp.DecodeBytes(data, &ptx); err != nil {
id, _ := p.lookup.storeidOfTx(hash)
log.Error("Blobs corrupted for traced transaction",
"hash", hash, "id", id, "err", err)
return nil
}
- return item
+ return ptx.ToTx()
}
-// GetRLP returns a RLP-encoded transaction if it is contained in the pool.
+// GetRLP returns a RLP-encoded transaction for network if it is contained in the pool.
+// It converts the pool's internal type to the RLP format used by the eth protocol:
+// e.g. type_byte || [..., version, [blobs], [comms], [proofs]]
func (p *BlobPool) GetRLP(hash common.Hash) []byte {
- return p.getRLP(hash)
+ data := p.getRLP(hash)
+ if len(data) == 0 {
+ // Not in this pool, do not log.
+ return nil
+ }
+ rlp, err := encodeForNetwork(data)
+ if err != nil {
+ log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err)
+ return nil
+ }
+ return rlp
}
// GetMetadata returns the transaction type and transaction size with the
@@ -1486,18 +1653,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
}
// Decode the blob transaction
- tx := new(types.Transaction)
- if err := rlp.DecodeBytes(data, tx); err != nil {
+ var ptx blobTxForPool
+ if err := rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err)
continue
}
- sidecar := tx.BlobTxSidecar()
- if sidecar == nil {
- log.Error("Blob tx without sidecar", "hash", tx.Hash(), "id", txID)
- continue
- }
+ sidecar := ptx.Sidecar()
// Traverse the blobs in the transaction
- for i, hash := range tx.BlobHashes() {
+ for i, hash := range ptx.Tx.BlobHashes() {
list, ok := indices[hash]
if !ok {
continue // non-interesting blob
@@ -1517,7 +1680,8 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
case types.BlobSidecarVersion1:
cellProofs, err := sidecar.CellProofsAt(i)
if err != nil {
- return nil, nil, nil, err
+ log.Error("Failed to get cell proofs", "id", txID, "err", err)
+ continue
}
pf = cellProofs
}
@@ -1596,9 +1760,10 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
// Store the tx in memory, and revalidate later
from, _ := types.Sender(p.signer, tx)
allowance := p.gappedAllowance(from)
- if allowance >= 1 && len(p.gapped) < maxGapped {
+ if allowance >= 1 && len(p.gappedSource) < maxGapped {
p.gapped[from] = append(p.gapped[from], tx)
p.gappedSource[tx.Hash()] = from
+ gappedGauge.Update(int64(len(p.gappedSource)))
log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
return nil
} else {
@@ -1606,6 +1771,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
// transactions by keeping the old and dropping this one.
// Thus replacing a gapped transaction with another gapped transaction
// is discouraged.
+ addGappedFullMeter.Mark(1)
log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
}
case errors.Is(err, core.ErrInsufficientFunds):
@@ -1641,7 +1807,8 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
}
// Transaction permitted into the pool from a nonce and cost perspective,
// insert it into the database and update the indices
- blob, err := rlp.EncodeToBytes(tx)
+ ptx := newBlobTxForPool(tx)
+ blob, err := rlp.EncodeToBytes(ptx)
if err != nil {
log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err)
return err
@@ -1650,7 +1817,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
if err != nil {
return err
}
- meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx)
+ meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), ptx)
var (
next = p.state.GetNonce(from)
@@ -1791,6 +1958,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
// We do not recurse here, but continue to loop instead.
// We are under lock, so we can add the transaction directly.
if err := p.addLocked(tx, false); err == nil {
+ gappedPromotedMeter.Mark(1)
log.Trace("Gapped blob transaction added to pool", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
} else {
log.Trace("Gapped blob transaction not accepted", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "err", err)
@@ -1802,6 +1970,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
} else {
p.gapped[from] = gtxs
}
+ gappedGauge.Update(int64(len(p.gappedSource)))
}
return nil
}
@@ -2069,8 +2238,9 @@ func (p *BlobPool) evictGapped() {
keep = append(keep, gtx)
}
}
- if len(keep) < len(txs) {
- log.Trace("Evicting old gapped blob transactions", "count", len(txs)-len(keep), "from", from)
+ if evicted := len(txs) - len(keep); evicted > 0 {
+ gappedEvictedMeter.Mark(int64(evicted))
+ log.Trace("Evicting old gapped blob transactions", "count", evicted, "from", from)
}
if len(keep) == 0 {
delete(p.gapped, from)
@@ -2078,6 +2248,7 @@ func (p *BlobPool) evictGapped() {
p.gapped[from] = keep
}
}
+ gappedGauge.Update(int64(len(p.gappedSource)))
}
// isAnnouncable checks whether a transaction is announcable based on its
diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go
index 7c57755401..8032e21e8a 100644
--- a/core/txpool/blobpool/blobpool_test.go
+++ b/core/txpool/blobpool/blobpool_test.go
@@ -235,6 +235,12 @@ func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64,
return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx)
}
+// encodeForPool encodes a blob transaction in the blobTxForPool storage format.
+func encodeForPool(tx *types.Transaction) []byte {
+ blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx))
+ return blob
+}
+
// makeMultiBlobTx is a utility method to construct a ramdom blob tx with
// certain number of blobs in its sidecar.
func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction {
@@ -530,7 +536,7 @@ func TestOpenDrops(t *testing.T) {
)
for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5
tx := makeTx(nonce, 1, 1, 1, gapper)
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
if nonce < 2 {
@@ -547,7 +553,7 @@ func TestOpenDrops(t *testing.T) {
)
for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling
tx := makeTx(nonce, 1, 1, 1, dangler)
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
dangling[id] = struct{}{}
@@ -560,7 +566,7 @@ func TestOpenDrops(t *testing.T) {
)
for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled
tx := makeTx(nonce, 1, 1, 1, filler)
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
filled[id] = struct{}{}
@@ -573,7 +579,7 @@ func TestOpenDrops(t *testing.T) {
)
for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled
tx := makeTx(nonce, 1, 1, 1, overlapper)
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
if nonce >= 2 {
@@ -595,7 +601,7 @@ func TestOpenDrops(t *testing.T) {
} else {
tx = makeTx(uint64(i), 1, 1, 1, underpayer)
}
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
underpaid[id] = struct{}{}
@@ -614,7 +620,7 @@ func TestOpenDrops(t *testing.T) {
} else {
tx = makeTx(uint64(i), 1, 1, 1, outpricer)
}
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
if i < 2 {
@@ -636,7 +642,7 @@ func TestOpenDrops(t *testing.T) {
} else {
tx = makeTx(nonce, 1, 1, 1, exceeder)
}
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
exceeded[id] = struct{}{}
@@ -654,7 +660,7 @@ func TestOpenDrops(t *testing.T) {
} else {
tx = makeTx(nonce, 1, 1, 1, overdrafter)
}
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
id, _ := store.Put(blob)
if nonce < 1 {
@@ -670,7 +676,7 @@ func TestOpenDrops(t *testing.T) {
overcapped = make(map[uint64]struct{})
)
for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ {
- blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, overcapper))
+ blob := encodeForPool(makeTx(nonce, 1, 1, 1, overcapper))
id, _ := store.Put(blob)
if nonce < maxTxsPerAccount {
@@ -686,7 +692,7 @@ func TestOpenDrops(t *testing.T) {
duplicated = make(map[uint64]struct{})
)
for _, nonce := range []uint64{0, 1, 2} {
- blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, duplicater))
+ blob := encodeForPool(makeTx(nonce, 1, 1, 1, duplicater))
for i := 0; i < int(nonce)+1; i++ {
id, _ := store.Put(blob)
@@ -705,7 +711,7 @@ func TestOpenDrops(t *testing.T) {
)
for _, nonce := range []uint64{0, 1, 2} {
for i := 0; i < int(nonce)+1; i++ {
- blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater))
+ blob := encodeForPool(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater))
id, _ := store.Put(blob)
if i == 0 {
@@ -842,7 +848,7 @@ func TestOpenIndex(t *testing.T) {
)
for _, i := range []int{5, 3, 4, 2, 0, 1} { // Randomize the tx insertion order to force sorting on load
tx := makeTx(uint64(i), txExecTipCaps[i], txExecFeeCaps[i], txBlobFeeCaps[i], key)
- blob, _ := rlp.EncodeToBytes(tx)
+ blob := encodeForPool(tx)
store.Put(blob)
}
store.Close()
@@ -934,9 +940,9 @@ func TestOpenHeap(t *testing.T) {
tx2 = makeTx(0, 1, 800, 70, key2)
tx3 = makeTx(0, 1, 1500, 110, key3)
- blob1, _ = rlp.EncodeToBytes(tx1)
- blob2, _ = rlp.EncodeToBytes(tx2)
- blob3, _ = rlp.EncodeToBytes(tx3)
+ blob1 = encodeForPool(tx1)
+ blob2 = encodeForPool(tx2)
+ blob3 = encodeForPool(tx3)
heapOrder = []common.Address{addr2, addr1, addr3}
heapIndex = map[common.Address]int{addr2: 0, addr1: 1, addr3: 2}
@@ -1009,9 +1015,9 @@ func TestOpenCap(t *testing.T) {
tx2 = makeTx(0, 1, 800, 70, key2)
tx3 = makeTx(0, 1, 1500, 110, key3)
- blob1, _ = rlp.EncodeToBytes(tx1)
- blob2, _ = rlp.EncodeToBytes(tx2)
- blob3, _ = rlp.EncodeToBytes(tx3)
+ blob1 = encodeForPool(tx1)
+ blob2 = encodeForPool(tx2)
+ blob3 = encodeForPool(tx3)
keep = []common.Address{addr1, addr3}
drop = []common.Address{addr2}
@@ -1098,8 +1104,8 @@ func TestChangingSlotterSize(t *testing.T) {
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0)
- blob1, _ = rlp.EncodeToBytes(tx1)
- blob2, _ = rlp.EncodeToBytes(tx2)
+ blob1 = encodeForPool(tx1)
+ blob2 = encodeForPool(tx2)
)
// Write the two safely sized txs to store. note: although the store is
@@ -1201,8 +1207,8 @@ func TestBillyMigration(t *testing.T) {
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0)
- blob1, _ = rlp.EncodeToBytes(tx1)
- blob2, _ = rlp.EncodeToBytes(tx2)
+ blob1 = encodeForPool(tx1)
+ blob2 = encodeForPool(tx2)
)
// Write the two safely sized txs to store. note: although the store is
@@ -1281,6 +1287,85 @@ func TestBillyMigration(t *testing.T) {
}
}
+// TestLegacyTxConversion verifies that on Init, transactions stored in the
+// legacy *types.Transaction RLP format are detected and migrated into the new
+// blobTxForPool storage format, and that they remain retrievable via the pool
+// API after the conversion.
+func TestLegacyTxConversion(t *testing.T) {
+ storage := t.TempDir()
+ os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
+ os.MkdirAll(filepath.Join(storage, limboedTransactionStore), 0700)
+
+ // Initialize the pending store with two blob transactions encoded in the
+ // legacy format.
+ queuedir := filepath.Join(storage, pendingTransactionStore)
+ store, err := billy.Open(billy.Options{Path: queuedir}, newSlotter(testMaxBlobsPerBlock), nil)
+ if err != nil {
+ t.Fatalf("failed to open billy: %v", err)
+ }
+
+ key1, _ := crypto.GenerateKey()
+ key2, _ := crypto.GenerateKey()
+ addr1 := crypto.PubkeyToAddress(key1.PublicKey)
+ addr2 := crypto.PubkeyToAddress(key2.PublicKey)
+
+ tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1, types.BlobSidecarVersion0)
+ tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2, types.BlobSidecarVersion0)
+
+ for _, tx := range []*types.Transaction{tx1, tx2} {
+ legacy, err := rlp.EncodeToBytes(tx)
+ if err != nil {
+ t.Fatalf("failed to legacy-encode tx: %v", err)
+ }
+ if _, err := store.Put(legacy); err != nil {
+ t.Fatalf("failed to put legacy blob: %v", err)
+ }
+ }
+ store.Close()
+
+ // Init should migrate the legacy entries into the new storage format.
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
+ statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
+ statedb.Commit(0, true, false)
+
+ chain := &testBlockChain{
+ config: params.MainnetChainConfig,
+ basefee: uint256.NewInt(params.InitialBaseFee),
+ blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
+ statedb: statedb,
+ }
+ pool := New(Config{Datadir: storage}, chain, nil)
+ if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
+ t.Fatalf("failed to create blob pool: %v", err)
+ }
+ defer pool.Close()
+
+ // Both transactions should be retrievable.
+ for _, want := range []*types.Transaction{tx1, tx2} {
+ got := pool.Get(want.Hash())
+ if got == nil {
+ t.Fatalf("migrated tx %s not found in pool", want.Hash())
+ }
+ if got.BlobTxSidecar() == nil {
+ t.Fatalf("migrated tx %s lost its sidecar", want.Hash())
+ }
+ if got.Hash() != want.Hash() {
+ t.Fatalf("migrated tx hash mismatch: have %s, want %s", got.Hash(), want.Hash())
+ }
+ }
+
+ // Legacy formats should not exist on pool.store
+ pool.store.Iterate(func(id uint64, size uint32, blob []byte) {
+ var ptx blobTxForPool
+ if err := rlp.DecodeBytes(blob, &ptx); err != nil {
+ t.Errorf("entry %d not in new blobTxForPool format: %v", id, err)
+ }
+ })
+
+ verifyPoolInternals(t, pool)
+}
+
// TestBlobCountLimit tests the blobpool enforced limits on the max blob count.
func TestBlobCountLimit(t *testing.T) {
var (
@@ -1746,7 +1831,7 @@ func TestAdd(t *testing.T) {
// Sign the seed transactions and store them in the data store
for _, tx := range seed.txs {
signed := types.MustSignNewTx(keys[acc], types.LatestSigner(params.MainnetChainConfig), tx)
- blob, _ := rlp.EncodeToBytes(signed)
+ blob := encodeForPool(signed)
store.Put(blob)
}
}
@@ -1853,9 +1938,9 @@ func TestGetBlobs(t *testing.T) {
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion1) // [6, 12)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3, types.BlobSidecarVersion0) // [12, 18)
- blob1, _ = rlp.EncodeToBytes(tx1)
- blob2, _ = rlp.EncodeToBytes(tx2)
- blob3, _ = rlp.EncodeToBytes(tx3)
+ blob1 = encodeForPool(tx1)
+ blob2 = encodeForPool(tx2)
+ blob3 = encodeForPool(tx3)
)
// Write the two safely sized txs to store. note: although the store is
@@ -2055,6 +2140,32 @@ func TestGetBlobs(t *testing.T) {
pool.Close()
}
+// TestEncodeForNetwork verifies that encodeForNetwork produces output identical
+// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars.
+func TestEncodeForNetwork(t *testing.T) {
+ t.Run("v0", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion0) })
+ t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion1) })
+}
+
+func testEncodeForNetwork(t *testing.T, version byte) {
+ key, _ := crypto.GenerateKey()
+ tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, version)
+
+ wantRLP, err := rlp.EncodeToBytes(tx)
+ if err != nil {
+ t.Fatalf("failed to encode tx: %v", err)
+ }
+ storedRLP := encodeForPool(tx)
+
+ gotRLP, err := encodeForNetwork(storedRLP)
+ if err != nil {
+ t.Fatalf("encodeForNetwork failed: %v", err)
+ }
+ if !bytes.Equal(gotRLP, wantRLP) {
+ t.Fatalf("network encoding mismatch (version %d): got %d bytes, want %d bytes", version, len(gotRLP), len(wantRLP))
+ }
+}
+
// fakeBilly is a billy.Database implementation which just drops data on the floor.
type fakeBilly struct {
billy.Database
diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go
index 36284d6a03..b8bee2f22a 100644
--- a/core/txpool/blobpool/limbo.go
+++ b/core/txpool/blobpool/limbo.go
@@ -33,7 +33,7 @@ import (
type limboBlob struct {
TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs
Block uint64 // Block in which the blob transaction was included
- Tx *types.Transaction
+ Ptx *blobTxForPool
}
// limbo is a light, indexed database to temporarily store recently included
@@ -146,15 +146,14 @@ func (l *limbo) finalize(final *types.Header) {
// push stores a new blob transaction into the limbo, waiting until finality for
// it to be automatically evicted.
-func (l *limbo) push(tx *types.Transaction, block uint64) error {
- // If the blobs are already tracked by the limbo, consider it a programming
- // error. There's not much to do against it, but be loud.
- if _, ok := l.index[tx.Hash()]; ok {
- log.Error("Limbo cannot push already tracked blobs", "tx", tx.Hash())
+func (l *limbo) push(ptx *blobTxForPool, block uint64) error {
+ hash := ptx.Tx.Hash()
+ if _, ok := l.index[hash]; ok {
+ log.Error("Limbo cannot push already tracked blobs", "tx", hash)
return errors.New("already tracked blob transaction")
}
- if err := l.setAndIndex(tx, block); err != nil {
- log.Error("Failed to set and index limboed blobs", "tx", tx.Hash(), "err", err)
+ if err := l.setAndIndex(ptx, block); err != nil {
+ log.Error("Failed to set and index limboed blobs", "tx", hash, "err", err)
return err
}
return nil
@@ -163,7 +162,7 @@ func (l *limbo) push(tx *types.Transaction, block uint64) error {
// pull retrieves a previously pushed set of blobs back from the limbo, removing
// it at the same time. This method should be used when a previously included blob
// transaction gets reorged out.
-func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) {
+func (l *limbo) pull(tx common.Hash) (*blobTxForPool, error) {
// If the blobs are not tracked by the limbo, there's not much to do. This
// can happen for example if a blob transaction is mined without pushing it
// into the network first.
@@ -177,7 +176,7 @@ func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) {
log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err)
return nil, err
}
- return item.Tx, nil
+ return item.Ptx, nil
}
// update changes the block number under which a blob transaction is tracked. This
@@ -209,7 +208,7 @@ func (l *limbo) update(txhash common.Hash, block uint64) {
log.Error("Failed to get and drop limboed blobs", "tx", txhash, "id", id, "err", err)
return
}
- if err := l.setAndIndex(item.Tx, block); err != nil {
+ if err := l.setAndIndex(item.Ptx, block); err != nil {
log.Error("Failed to set and index limboed blobs", "tx", txhash, "err", err)
return
}
@@ -240,12 +239,12 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) {
// setAndIndex assembles a limbo blob database entry and stores it, also updating
// the in-memory indices.
-func (l *limbo) setAndIndex(tx *types.Transaction, block uint64) error {
- txhash := tx.Hash()
+func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error {
+ txhash := ptx.Tx.Hash()
item := &limboBlob{
TxHash: txhash,
Block: block,
- Tx: tx,
+ Ptx: ptx,
}
data, err := rlp.EncodeToBytes(item)
if err != nil {
diff --git a/core/txpool/blobpool/metrics.go b/core/txpool/blobpool/metrics.go
index 52419ade09..44e2098b22 100644
--- a/core/txpool/blobpool/metrics.go
+++ b/core/txpool/blobpool/metrics.go
@@ -97,9 +97,15 @@ var (
addUnderpricedMeter = metrics.NewRegisteredMeter("blobpool/add/underpriced", nil) // Gas tip too low, neutral
addStaleMeter = metrics.NewRegisteredMeter("blobpool/add/stale", nil) // Nonce already filled, reject, bad-ish
addGappedMeter = metrics.NewRegisteredMeter("blobpool/add/gapped", nil) // Nonce gapped, reject, bad-ish
+ addGappedFullMeter = metrics.NewRegisteredMeter("blobpool/add/gappedfull", nil) // Gapped queue full, reject, neutral
addOverdraftedMeter = metrics.NewRegisteredMeter("blobpool/add/overdrafted", nil) // Balance exceeded, reject, neutral
addOvercappedMeter = metrics.NewRegisteredMeter("blobpool/add/overcapped", nil) // Per-account cap exceeded, reject, neutral
addNoreplaceMeter = metrics.NewRegisteredMeter("blobpool/add/noreplace", nil) // Replacement fees or tips too low, neutral
addNonExclusiveMeter = metrics.NewRegisteredMeter("blobpool/add/nonexclusive", nil) // Plain transaction from same account exists, reject, neutral
addValidMeter = metrics.NewRegisteredMeter("blobpool/add/valid", nil) // Valid transaction, add, neutral
+
+ // Gapped queue metrics for observability
+ gappedGauge = metrics.NewRegisteredGauge("blobpool/gapped/count", nil) // Current gapped queue size
+ gappedPromotedMeter = metrics.NewRegisteredMeter("blobpool/gapped/promoted", nil) // Gapped txs successfully promoted to pool
+ gappedEvictedMeter = metrics.NewRegisteredMeter("blobpool/gapped/evicted", nil) // Gapped txs evicted due to timeout/stale
)
diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go
index 00630de04c..3d66803fd7 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -467,8 +467,8 @@ func (pool *LegacyPool) stats() (int, int) {
// Content retrieves the data content of the transaction pool, returning all the
// pending as well as queued transactions, grouped by account and sorted by nonce.
func (pool *LegacyPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) {
- pool.mu.Lock()
- defer pool.mu.Unlock()
+ pool.mu.RLock()
+ defer pool.mu.RUnlock()
pending := make(map[common.Address][]*types.Transaction, len(pool.pending))
for addr, list := range pool.pending {
@@ -503,8 +503,8 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) (map[common.Address
if filter.BlobTxs {
return nil, 0
}
- pool.mu.Lock()
- defer pool.mu.Unlock()
+ pool.mu.RLock()
+ defer pool.mu.RUnlock()
var count int
pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending))
diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go
index bb178f175e..66f3248105 100644
--- a/core/txpool/locals/tx_tracker.go
+++ b/core/txpool/locals/tx_tracker.go
@@ -18,6 +18,7 @@
package locals
import (
+ "cmp"
"slices"
"sync"
"time"
@@ -151,7 +152,7 @@ func (tracker *TxTracker) recheck(journalCheck bool) []*types.Transaction {
for _, list := range rejournal {
// cmp(a, b) should return a negative number when a < b,
slices.SortFunc(list, func(a, b *types.Transaction) int {
- return int(a.Nonce() - b.Nonce())
+ return cmp.Compare(a.Nonce(), b.Nonce())
})
}
// Rejournal the tracker while holding the lock. No new transactions will
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index 6891dc94d2..c87bba31ac 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -125,7 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata
- intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai)
+ intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
if err != nil {
return err
}
@@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the transaction can cover floor data gas.
if rules.IsPrague {
- floorDataGas, err := core.FloorDataGas(rules, tx.Data())
+ floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
return err
}
diff --git a/core/types.go b/core/types.go
index 87bbfcff58..edbfc43db3 100644
--- a/core/types.go
+++ b/core/types.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
)
@@ -58,4 +59,8 @@ type ProcessResult struct {
Requests [][]byte
Logs []*types.Log
GasUsed uint64
+
+ // BAL is only meaningful for post-Amsterdam blocks. Please ensure
+ // fork validation is performed before accessing it.
+ Bal *bal.ConstructionBlockAccessList
}
diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go
deleted file mode 100644
index e563fa22e2..0000000000
--- a/core/types/bal/access_list.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2026 The go-ethereum Authors
-// This file is part of the go-ethereum library.
-//
-// The go-ethereum library is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// The go-ethereum library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see
-
-package bal
-
-import (
- "maps"
-
- "github.com/ethereum/go-ethereum/common"
-)
-
-// StorageAccessList represents a set of storage slots accessed within an account.
-type StorageAccessList map[common.Hash]struct{}
-
-// StateAccessList records the set of accounts and storage slots that have been
-// accessed. An entry with an empty StorageAccessList denotes an account access
-// without any storage slot access.
-type StateAccessList struct {
- list map[common.Address]StorageAccessList
-}
-
-// NewStateAccessList returns an empty StateAccessList ready for use.
-func NewStateAccessList() *StateAccessList {
- return &StateAccessList{
- list: make(map[common.Address]StorageAccessList),
- }
-}
-
-// AddAccount records an access to the given account. It is a no-op if the
-// account is already present.
-func (s *StateAccessList) AddAccount(addr common.Address) {
- if s == nil {
- return
- }
- if _, exists := s.list[addr]; !exists {
- s.list[addr] = make(StorageAccessList)
- }
-}
-
-// AddState records an access to the given storage slot. The owning account is
-// implicitly recorded as well.
-func (s *StateAccessList) AddState(addr common.Address, slot common.Hash) {
- if s == nil {
- return
- }
- slots, exists := s.list[addr]
- if !exists {
- slots = make(StorageAccessList)
- s.list[addr] = slots
- }
- slots[slot] = struct{}{}
-}
-
-// Merge merges the entries from other into the receiver.
-func (s *StateAccessList) Merge(other *StateAccessList) {
- if s == nil || other == nil {
- return
- }
- for addr, otherSlots := range other.list {
- slots, exists := s.list[addr]
- if !exists {
- s.list[addr] = otherSlots
- continue
- }
- maps.Copy(slots, otherSlots)
- }
-}
-
-// Copy returns a deep copy of the StateAccessList.
-func (s *StateAccessList) Copy() *StateAccessList {
- if s == nil {
- return nil
- }
- cpy := &StateAccessList{
- list: make(map[common.Address]StorageAccessList, len(s.list)),
- }
- for addr, slots := range s.list {
- cpy.list[addr] = maps.Clone(slots)
- }
- return cpy
-}
diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go
index 99ead8d6f0..2eb5fe93cd 100644
--- a/core/types/bal/bal.go
+++ b/core/types/bal/bal.go
@@ -71,8 +71,8 @@ type ConstructionBlockAccessList struct {
}
// NewConstructionBlockAccessList instantiates an empty access list.
-func NewConstructionBlockAccessList() ConstructionBlockAccessList {
- return ConstructionBlockAccessList{
+func NewConstructionBlockAccessList() *ConstructionBlockAccessList {
+ return &ConstructionBlockAccessList{
Accounts: make(map[common.Address]*ConstructionAccountAccess),
}
}
@@ -138,10 +138,62 @@ func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common
// PrettyPrint returns a human-readable representation of the access list
func (b *ConstructionBlockAccessList) PrettyPrint() string {
- enc := b.toEncodingObj()
+ enc := b.ToEncodingObj()
return enc.PrettyPrint()
}
+// Merge applies other on top of the local block access list. For colliding
+// entries (a (slot, txIdx) write or a txIdx-keyed balance/nonce/code change),
+// the value from other wins, matching the semantics of applying the local
+// effects first and then other's. Storage reads are unioned; any slot
+// written by either side is dropped from StorageReads.
+//
+// Typically each list covers its own tx index, so txIdx-level collisions are
+// not expected; the exception is pre/post-transition system calls, which
+// share a single tx index. In that case callers must pass block-accessList
+// in order strictly.
+//
+// other is referenced (not deep copied), after the call both lists share
+// inner maps and other must not be mutated.
+func (b *ConstructionBlockAccessList) Merge(other *ConstructionBlockAccessList) {
+ if other == nil {
+ return
+ }
+ for addr, otherAcc := range other.Accounts {
+ acc, ok := b.Accounts[addr]
+ if !ok {
+ b.Accounts[addr] = otherAcc
+ continue
+ }
+ for key, writes := range otherAcc.StorageWrites {
+ existing, ok := acc.StorageWrites[key]
+ if !ok {
+ acc.StorageWrites[key] = writes
+ } else {
+ for txIdx, value := range writes {
+ existing[txIdx] = value
+ }
+ }
+ delete(acc.StorageReads, key)
+ }
+ for key := range otherAcc.StorageReads {
+ if _, ok := acc.StorageWrites[key]; ok {
+ continue
+ }
+ acc.StorageReads[key] = struct{}{}
+ }
+ for txIdx, balance := range otherAcc.BalanceChanges {
+ acc.BalanceChanges[txIdx] = balance
+ }
+ for txIdx, nonce := range otherAcc.NonceChanges {
+ acc.NonceChanges[txIdx] = nonce
+ }
+ for txIdx, code := range otherAcc.CodeChange {
+ acc.CodeChange[txIdx] = code
+ }
+ }
+}
+
// Copy returns a deep copy of the access list.
func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
res := NewConstructionBlockAccessList()
@@ -169,5 +221,5 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
aaCopy.CodeChange = codes
res.Accounts[addr] = &aaCopy
}
- return &res
+ return res
}
diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go
index 03f97f3809..399f9db7c0 100644
--- a/core/types/bal/bal_encoding.go
+++ b/core/types/bal/bal_encoding.go
@@ -78,17 +78,43 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error {
// Validate returns an error if the contents of the access list are not ordered
// according to the spec or any code changes are contained which exceed protocol
// max code size.
-func (e *BlockAccessList) Validate(rules params.Rules) error {
+func (e *BlockAccessList) Validate(blockGasLimit uint64) error {
if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int {
return bytes.Compare(a.Address[:], b.Address[:])
}) {
return errors.New("block access list accounts not in lexicographic order")
}
for _, entry := range *e {
- if err := entry.validate(rules); err != nil {
+ if err := entry.validate(); err != nil {
return err
}
}
+ return e.ValidateSize(blockGasLimit)
+}
+
+// itemCount returns the number of items in the BAL for EIP-7928 size-constraint
+// purposes: the count of distinct addresses plus every storage key (writes +
+// reads) carried by those accounts. A storage slot is counted once regardless
+// of how many transactions wrote to it.
+func (e *BlockAccessList) itemCount() uint64 {
+ count := uint64(len(*e)) // distinct addresses
+ for i := range *e {
+ count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads))
+ }
+ return count
+}
+
+// ValidateSize returns an error if the BAL violates the EIP-7928 size
+// constraint for the given block gas limit:
+//
+// itemCount() <= blockGasLimit / params.BALItemCost
+func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error {
+ items := e.itemCount()
+ limit := blockGasLimit / params.BALItemCost
+ if items > limit {
+ return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)",
+ items, limit, blockGasLimit, params.BALItemCost)
+ }
return nil
}
@@ -159,7 +185,7 @@ type AccountAccess struct {
// validate converts the account accesses out of encoding format.
// If any of the keys in the encoding object are not ordered according to the
// spec, an error is returned.
-func (e *AccountAccess) validate(rules params.Rules) error {
+func (e *AccountAccess) validate() error {
// Check the storage write slots are sorted in order
if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int {
return a.Slot.Cmp(b.Slot)
@@ -200,14 +226,7 @@ func (e *AccountAccess) validate(rules params.Rules) error {
return errors.New("code changes not in ascending order by tx index")
}
for _, change := range e.CodeChanges {
- var sizeLimit int
- switch {
- case rules.IsAmsterdam:
- sizeLimit = params.MaxCodeSizeAmsterdam
- default:
- sizeLimit = params.MaxCodeSize
- }
- if len(change.Code) > sizeLimit {
+ if len(change.Code) > params.MaxCodeSizeAmsterdam {
return errors.New("code change contained oversized code")
}
}
@@ -257,7 +276,7 @@ func (e *AccountAccess) Copy() AccountAccess {
// EncodeRLP returns the RLP-encoded access list
func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error {
- return b.toEncodingObj().EncodeRLP(wr)
+ return b.ToEncodingObj().EncodeRLP(wr)
}
var _ rlp.Encoder = &ConstructionBlockAccessList{}
@@ -340,9 +359,9 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
return res
}
-// toEncodingObj returns an instance of the access list expressed as the type
+// ToEncodingObj returns an instance of the access list expressed as the type
// which is used as input for the encoding/decoding.
-func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList {
+func (b *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList {
var addresses []common.Address
for addr := range b.Accounts {
addresses = append(addresses, addr)
diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go
index 32a0292f2e..2b6a3c194e 100644
--- a/core/types/bal/bal_test.go
+++ b/core/types/bal/bal_test.go
@@ -19,6 +19,7 @@ package bal
import (
"bytes"
"cmp"
+ "math"
"reflect"
"slices"
"testing"
@@ -98,14 +99,65 @@ func TestBALEncoding(t *testing.T) {
if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil {
t.Fatalf("decoding failed: %v\n", err)
}
- if dec.Hash() != bal.toEncodingObj().Hash() {
+ if dec.Hash() != bal.ToEncodingObj().Hash() {
t.Fatalf("encoded block hash doesn't match decoded")
}
- if !reflect.DeepEqual(bal.toEncodingObj(), &dec) {
+ if !reflect.DeepEqual(bal.ToEncodingObj(), &dec) {
t.Fatal("decoded BAL doesn't match")
}
}
+func TestConstructionBALMerge(t *testing.T) {
+ var (
+ addrA = common.BytesToAddress([]byte{0xAA})
+ addrB = common.BytesToAddress([]byte{0xBB})
+ slot1 = common.BytesToHash([]byte{0x01})
+ slot2 = common.BytesToHash([]byte{0x02})
+ slot3 = common.BytesToHash([]byte{0x03})
+ )
+ a := NewConstructionBlockAccessList()
+ a.StorageWrite(1, addrA, slot1, common.BytesToHash([]byte{0x11}))
+ a.StorageRead(addrA, slot2) // demoted by other's write below
+ a.BalanceChange(1, addrA, uint256.NewInt(100))
+ a.NonceChange(addrA, 1, 7)
+
+ b := NewConstructionBlockAccessList()
+ b.StorageWrite(2, addrA, slot1, common.BytesToHash([]byte{0x22})) // same slot, disjoint txIdx
+ b.StorageWrite(2, addrA, slot2, common.BytesToHash([]byte{0x33}))
+ b.StorageRead(addrA, slot3)
+ b.BalanceChange(2, addrA, uint256.NewInt(200))
+ b.NonceChange(addrA, 2, 8)
+ b.CodeChange(addrB, 2, []byte{0xde, 0xad}) // account only in other
+
+ a.Merge(b)
+
+ accA := a.Accounts[addrA]
+ wantWrites := map[common.Hash]map[uint32]common.Hash{
+ slot1: {1: common.BytesToHash([]byte{0x11}), 2: common.BytesToHash([]byte{0x22})},
+ slot2: {2: common.BytesToHash([]byte{0x33})},
+ }
+ if !reflect.DeepEqual(accA.StorageWrites, wantWrites) {
+ t.Fatalf("storage writes mismatch: got %v, want %v", accA.StorageWrites, wantWrites)
+ }
+ wantReads := map[common.Hash]struct{}{slot3: {}}
+ if !reflect.DeepEqual(accA.StorageReads, wantReads) {
+ t.Fatalf("storage reads mismatch: got %v, want %v", accA.StorageReads, wantReads)
+ }
+ if accA.BalanceChanges[1].Uint64() != 100 || accA.BalanceChanges[2].Uint64() != 200 {
+ t.Fatalf("balance changes mismatch: %v", accA.BalanceChanges)
+ }
+ if accA.NonceChanges[1] != 7 || accA.NonceChanges[2] != 8 {
+ t.Fatalf("nonce changes mismatch: %v", accA.NonceChanges)
+ }
+ accB, ok := a.Accounts[addrB]
+ if !ok {
+ t.Fatal("account only present in other was not adopted")
+ }
+ if !bytes.Equal(accB.CodeChange[2], []byte{0xde, 0xad}) {
+ t.Fatalf("code change for adopted account missing: %x", accB.CodeChange[2])
+ }
+}
+
func makeTestAccountAccess(sort bool) AccountAccess {
var (
storageWrites []encodingSlotWrites
@@ -231,10 +283,82 @@ func TestBlockAccessListCopy(t *testing.T) {
}
}
+func TestBlockAccessListItemCount(t *testing.T) {
+ empty := &BlockAccessList{}
+ if got := empty.itemCount(); got != 0 {
+ t.Fatalf("empty BAL item count: got %d, want 0", got)
+ }
+
+ addr1 := [20]byte(testrand.Bytes(20))
+ addr2 := [20]byte(testrand.Bytes(20))
+ one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) }
+ bal := &BlockAccessList{
+ AccountAccess{
+ Address: addr1,
+ StorageWrites: []encodingSlotWrites{
+ {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}},
+ {Slot: one()},
+ },
+ StorageReads: []*uint256.Int{one()},
+ },
+ AccountAccess{Address: addr2}, // address-only, no slots
+ }
+ // 2 addresses + 2 write-slots + 1 read-slot = 5 items.
+ // (Multiple TxIdx writes to the same slot count as ONE item.)
+ if got := bal.itemCount(); got != 5 {
+ t.Fatalf("item count: got %d, want 5", got)
+ }
+}
+
+func TestBlockAccessListValidateSize(t *testing.T) {
+ // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage
+ // slots (some writes, some reads). 3 + 9*3 = 30.
+ one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) }
+ bal := make(BlockAccessList, 3)
+ for i := range bal {
+ bal[i].Address = [20]byte(testrand.Bytes(20))
+ for j := 0; j < 5; j++ {
+ bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{
+ Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}},
+ })
+ }
+ for j := 0; j < 4; j++ {
+ bal[i].StorageReads = append(bal[i].StorageReads, one())
+ }
+ }
+ if got := bal.itemCount(); got != 30 {
+ t.Fatalf("setup: item count = %d, want 30", got)
+ }
+
+ // limit = blockGasLimit / BALItemCost.
+ // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000.
+ tests := []struct {
+ name string
+ gasLimit uint64
+ expectError bool
+ }{
+ {"exactly at limit", 30 * params.BALItemCost, false},
+ {"well above limit", 60_000_000, false},
+ {"one below limit", 30*params.BALItemCost - 1, true},
+ {"zero gas limit", 0, true},
+ }
+ for _, tc := range tests {
+ err := bal.ValidateSize(tc.gasLimit)
+ if (err != nil) != tc.expectError {
+ t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError)
+ }
+ }
+
+ // Empty BAL is always valid (even with 0 gas limit).
+ if err := (&BlockAccessList{}).ValidateSize(0); err != nil {
+ t.Fatalf("empty BAL must pass any limit: %v", err)
+ }
+}
+
func TestBlockAccessListValidation(t *testing.T) {
// Validate the block access list after RLP decoding
enc := makeTestBAL(true)
- if err := enc.Validate(params.Rules{}); err != nil {
+ if err := enc.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
var buf bytes.Buffer
@@ -246,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) {
if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil {
t.Fatalf("Unexpected RLP-decode error: %v", err)
}
- if err := dec.Validate(params.Rules{}); err != nil {
+ if err := dec.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
// Validate the derived block access list
cBAL := makeTestConstructionBAL()
- listB := cBAL.toEncodingObj()
- if err := listB.Validate(params.Rules{}); err != nil {
+ listB := cBAL.ToEncodingObj()
+ if err := listB.Validate(math.MaxUint64); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
}
diff --git a/core/types/block.go b/core/types/block.go
index ea576ed232..0856845a4e 100644
--- a/core/types/block.go
+++ b/core/types/block.go
@@ -413,8 +413,9 @@ func (b *Block) BaseFee() *big.Int {
return new(big.Int).Set(b.header.BaseFee)
}
-func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot }
-func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash }
+func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot }
+func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash }
+func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash }
func (b *Block) ExcessBlobGas() *uint64 {
var excessBlobGas *uint64
diff --git a/core/types/hashes.go b/core/types/hashes.go
index db8912a66f..541681e4db 100644
--- a/core/types/hashes.go
+++ b/core/types/hashes.go
@@ -43,6 +43,9 @@ var (
// EmptyRequestsHash is the known hash of an empty request set, sha256("").
EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
+ // EmptyBlockAccessListHash is the known hash of an empty block accessList, keccak256(rlp.encode([])).
+ EmptyBlockAccessListHash = common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
+
// EmptyBinaryHash is the known hash of an empty binary trie.
EmptyBinaryHash = common.Hash{}
)
diff --git a/core/vm/contract.go b/core/vm/contract.go
index a55a5dde8b..45c879c80f 100644
--- a/core/vm/contract.go
+++ b/core/vm/contract.go
@@ -131,8 +131,8 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G
if !ok {
return false
}
- if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
- logger.OnGasChange(prior, c.Gas.RegularGas, reason)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
return true
}
@@ -143,8 +143,8 @@ func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tra
if !changed {
return
}
- if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
- logger.OnGasChange(prior, c.Gas.RegularGas, reason)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
}
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index 6dadb64873..71cfdbc527 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -269,8 +269,8 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm
gas.Exhaust()
return nil, gas, ErrOutOfGas
}
- if logger != nil && logger.OnGasChange != nil {
- logger.OnGasChange(prior, gas.RegularGas, tracing.GasChangeCallPrecompiledContract)
+ if logger.HasGasHook() {
+ logger.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeCallPrecompiledContract)
}
// Touch the precompile for block-level accessList recording once Amsterdam
// fork is activated.
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 26b2f73a00..832306b9a0 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -317,8 +317,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@@ -371,8 +371,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@@ -415,8 +415,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@@ -470,8 +470,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@@ -509,8 +509,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
gas.Exhaust()
return nil, common.Address{}, gas, ErrOutOfGas
}
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractCollisionCheck)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
}
}
@@ -528,8 +528,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
return nil, common.Address{}, gas, ErrContractAddressCollision
@@ -558,8 +558,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
return nil, common.Address{}, gas, ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractInit)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractInit)
}
}
evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)
@@ -673,15 +673,17 @@ func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to comm
if tracer.OnEnter != nil {
tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value)
}
- if tracer.OnGasChange != nil {
- tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance)
+ if tracer.HasGasHook() {
+ initial := NewGasBudget(startGas)
+ tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance)
}
}
func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) {
tracer := evm.Config.Tracer
- if leftOverGas != 0 && tracer.OnGasChange != nil {
- tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned)
+ if leftOverGas != 0 && tracer.HasGasHook() {
+ leftover := NewGasBudget(leftOverGas)
+ tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
}
var reverted bool
if err != nil {
@@ -707,3 +709,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext {
StateDB: evm.StateDB,
}
}
+
+// GetRules returns the chain rules used throughout the EVM execution.
+func (evm *EVM) GetRules() params.Rules {
+ return evm.chainRules
+}
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index cc90c54798..ed938ae41f 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -16,7 +16,11 @@
package vm
-import "fmt"
+import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/core/tracing"
+)
// GasCosts denotes a vector of gas costs in the
// multidimensional metering paradigm. It represents the cost
@@ -77,21 +81,26 @@ func (g GasBudget) CanAfford(cost GasCosts) bool {
}
// Charge deducts the given gas cost from the budget. It returns the
-// pre-charge gas value and false if the budget does not have sufficient
+// pre-charge budget and false if the budget does not have sufficient
// gas to cover the cost.
-func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) {
- prior := g.RegularGas
- if prior < cost.RegularGas {
+func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
+ prior := *g
+ if g.RegularGas < cost.RegularGas {
return prior, false
}
g.RegularGas -= cost.RegularGas
return prior, true
}
-// Refund adds the given gas budget back. It returns the pre-refund gas
-// value and whether the budget was actually changed.
-func (g *GasBudget) Refund(other GasBudget) (uint64, bool) {
- prior := g.RegularGas
+// Refund adds the given gas budget back. It returns the pre-refund budget
+// and whether the budget was actually changed.
+func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) {
+ prior := *g
g.RegularGas += other.RegularGas
- return prior, g.RegularGas != prior
+ return prior, g.RegularGas != prior.RegularGas
+}
+
+// AsTracing converts the GasBudget into the tracing-facing Gas vector.
+func (g GasBudget) AsTracing() tracing.Gas {
+ return tracing.Gas{Regular: g.RegularGas, State: g.StateGas}
}
diff --git a/core/vm/interface.go b/core/vm/interface.go
index 487d8002f9..a9938c2a28 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -98,5 +98,6 @@ type StateDB interface {
AccessEvents() *state.AccessEvents
// Finalise must be invoked at the end of a transaction
- Finalise(bool) *bal.StateAccessList
+ Finalise(bool) *bal.ConstructionBlockAccessList
+ SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32)
}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 4c278fc857..3994327247 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -234,8 +234,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// Do tracing before potential memory expansion
if debug {
- if evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(
+ tracing.Gas{Regular: gasCopy, State: contract.Gas.StateGas},
+ tracing.Gas{Regular: gasCopy - cost, State: contract.Gas.StateGas},
+ tracing.GasChangeCallOpCode,
+ )
}
if evm.Config.Tracer.OnOpcode != nil {
evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))
diff --git a/crypto/signature_nocgo.go b/crypto/signature_nocgo.go
index 0aab7180d3..bf273612e9 100644
--- a/crypto/signature_nocgo.go
+++ b/crypto/signature_nocgo.go
@@ -103,7 +103,7 @@ func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) {
// The public key should be in compressed (33 bytes) or uncompressed (65 bytes) format.
// The signature should have the 64 byte [R || S] format.
func VerifySignature(pubkey, hash, signature []byte) bool {
- if len(signature) != 64 {
+ if len(signature) != 64 || len(hash) != DigestLength {
return false
}
var r, s secp256k1.ModNScalar
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 33fe4fe5d9..5e3558d8eb 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
+ "github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/filtermaps"
@@ -61,9 +62,9 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header {
return b.eth.blockchain.CurrentBlock()
}
-func (b *EthAPIBackend) SetHead(number uint64) {
+func (b *EthAPIBackend) SetHead(number uint64) error {
b.eth.handler.downloader.Cancel()
- b.eth.blockchain.SetHead(number)
+ return b.eth.blockchain.SetHead(number)
}
func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
@@ -430,6 +431,13 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB
return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
}
+func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int {
+ if b.ChainConfig().IsLondon(b.CurrentHeader().Number) {
+ return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader())
+ }
+ return nil
+}
+
func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int {
if excess := b.CurrentHeader().ExcessBlobGas; excess != nil {
return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader())
diff --git a/eth/backend.go b/eth/backend.go
index 08a3c70c9d..af8b04bda6 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -49,7 +49,6 @@ import (
"github.com/ethereum/go-ethereum/eth/protocols/snap"
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethdb"
- "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/shutdowncheck"
"github.com/ethereum/go-ethereum/internal/version"
@@ -105,7 +104,6 @@ type Ethereum struct {
// DB interfaces
chainDb ethdb.Database // Block chain database
- eventMux *event.TypeMux
engine consensus.Engine
accountManager *accounts.Manager
@@ -194,7 +192,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
eth := &Ethereum{
config: config,
chainDb: chainDb,
- eventMux: stack.EventMux(),
accountManager: stack.AccountManager(),
engine: engine,
networkID: networkID,
@@ -237,6 +234,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
StateHistory: config.StateHistory,
TrienodeHistory: config.TrienodeHistory,
NodeFullValueCheckpoint: config.NodeFullValueCheckpoint,
+ BinTrieGroupDepth: config.BinTrieGroupDepth,
StateScheme: scheme,
HistoryPolicy: histPolicy,
TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)),
@@ -343,7 +341,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
Network: networkID,
Sync: config.SyncMode,
BloomCache: uint64(cacheLimit),
- EventMux: eth.eventMux,
RequiredBlocks: config.RequiredBlocks,
}); err != nil {
return nil, err
@@ -404,7 +401,7 @@ func (s *Ethereum) APIs() []rpc.API {
Service: NewMinerAPI(s),
}, {
Namespace: "eth",
- Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain, s.eventMux),
+ Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain),
}, {
Namespace: "admin",
Service: NewAdminAPI(s),
@@ -599,7 +596,6 @@ func (s *Ethereum) Stop() error {
s.shutdownTracker.Stop()
s.chainDb.Close()
- s.eventMux.Stop()
return nil
}
diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go
index be19bd5de4..3861f62813 100644
--- a/eth/catalyst/api.go
+++ b/eth/catalyst/api.go
@@ -82,6 +82,9 @@ const (
// beaconUpdateWarnFrequency is the frequency at which to warn the user that
// the beacon client is offline.
beaconUpdateWarnFrequency = 5 * time.Minute
+
+ // maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated.
+ maxReorgDepth = 32
)
type ConsensusAPI struct {
@@ -237,6 +240,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.
func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes, payloadVersion engine.PayloadVersion, payloadWitness bool) (result engine.ForkChoiceResponse, err error) {
ctx, _, spanEnd := telemetry.StartSpan(ctx, "engine.forkchoiceUpdated")
defer spanEnd(&err)
+
api.forkchoiceLock.Lock()
defer api.forkchoiceLock.Unlock()
@@ -321,10 +325,23 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
// generating the payload. It's a special corner case that a few slots are
// missing and we are requested to generate the payload in slot.
} else {
- // If the head block is already in our canonical chain, the beacon client is
- // probably resyncing. Ignore the update.
- log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number)
- return valid(nil), nil
+ if finalized := api.eth.BlockChain().CurrentFinalBlock(); finalized != nil && block.NumberU64() <= finalized.Number.Uint64() {
+ log.Info("Skipping beacon update to finalized ancestor", "number", block.NumberU64(), "hash", update.HeadBlockHash)
+ return valid(nil), nil
+ }
+ depth := api.eth.BlockChain().CurrentBlock().Number.Uint64() - block.NumberU64()
+ if depth >= maxReorgDepth {
+ log.Warn("Refusing too deep reorg", "depth", depth, "head", update.HeadBlockHash)
+ return engine.STATUS_INVALID, engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds limit %d", depth, maxReorgDepth))
+ }
+ if !api.eth.Synced() {
+ log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash)
+ return valid(nil), nil
+ }
+ if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
+ log.Error("Error setting canonical", "number", block.NumberU64(), "hash", update.HeadBlockHash, "error", err)
+ return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err
+ }
}
api.eth.SetSynced()
@@ -629,6 +646,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd
return nil, engine.InvalidParams.With(err)
}
// Validate the blobs from the pool and assemble the response
+ filled := 0
res := make(engine.BlobAndProofListV2, len(hashes))
for i := range blobs {
// The blob has been evicted since the last AvailableBlobs call.
@@ -649,10 +667,11 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd
Blob: blobs[i][:],
CellProofs: cellProofs,
}
+ filled++
}
- if len(res) == len(hashes) {
+ if filled == len(hashes) {
getBlobsRequestCompleteHit.Inc(1)
- } else if len(res) > 0 {
+ } else if filled > 0 {
getBlobsRequestPartialHit.Inc(1)
} else {
getBlobsRequestMiss.Inc(1)
diff --git a/eth/downloader/api.go b/eth/downloader/api.go
index 1fea35775e..6033e44474 100644
--- a/eth/downloader/api.go
+++ b/eth/downloader/api.go
@@ -23,7 +23,6 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/core"
- "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/rpc"
)
@@ -33,20 +32,18 @@ import (
type DownloaderAPI struct {
d *Downloader
chain *core.BlockChain
- mux *event.TypeMux
installSyncSubscription chan chan interface{}
uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest
}
// NewDownloaderAPI creates a new DownloaderAPI. The API has an internal event loop that
-// listens for events from the downloader through the global event mux. In case it receives one of
+// listens for events from the downloader through the event feed. In case it receives one of
// these events it broadcasts it to all syncing subscriptions that are installed through the
// installSyncSubscription channel.
-func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) *DownloaderAPI {
+func NewDownloaderAPI(d *Downloader, chain *core.BlockChain) *DownloaderAPI {
api := &DownloaderAPI{
d: d,
chain: chain,
- mux: m,
installSyncSubscription: make(chan chan interface{}),
uninstallSyncSubscription: make(chan *uninstallSyncSubscriptionRequest),
}
@@ -66,7 +63,8 @@ func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) *
// receive is {false}.
func (api *DownloaderAPI) eventLoop() {
var (
- sub = api.mux.Subscribe(StartEvent{})
+ events = make(chan SyncEvent, 16)
+ sub = api.d.SubscribeSyncEvents(events)
syncSubscriptions = make(map[chan interface{}]struct{})
checkInterval = time.Second * 60
checkTimer = time.NewTimer(checkInterval)
@@ -90,6 +88,7 @@ func (api *DownloaderAPI) eventLoop() {
}
)
defer checkTimer.Stop()
+ defer sub.Unsubscribe()
for {
select {
@@ -101,14 +100,13 @@ func (api *DownloaderAPI) eventLoop() {
case u := <-api.uninstallSyncSubscription:
delete(syncSubscriptions, u.c)
close(u.uninstalled)
- case event := <-sub.Chan():
- if event == nil {
- return
- }
- switch event.Data.(type) {
- case StartEvent:
+ case ev := <-events:
+ if ev.Type == SyncStarted {
started = true
}
+ case <-sub.Err():
+ // The downloader is terminated or other internal error occurs
+ return
case <-checkTimer.C:
if !started {
checkTimer.Reset(checkInterval)
diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go
index 1de0933842..4a575d6856 100644
--- a/eth/downloader/downloader.go
+++ b/eth/downloader/downloader.go
@@ -97,9 +97,12 @@ type headerTask struct {
}
type Downloader struct {
- mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode
- moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle
- mux *event.TypeMux // Event multiplexer to announce sync operation events
+ mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode
+ moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle
+
+ // Event feed for downloader events
+ feed event.FeedOf[SyncEvent]
+ scope event.SubscriptionScope
queue *queue // Scheduler for selecting the hashes to download
peers *peerSet // Set of active peers from which download can proceed
@@ -229,12 +232,11 @@ type BlockChain interface {
}
// New creates a new downloader to fetch hashes and blocks from remote peers.
-func New(stateDb ethdb.Database, mode ethconfig.SyncMode, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader {
+func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader {
cutoffNumber, cutoffHash := chain.HistoryPruningCutoff()
dl := &Downloader{
stateDB: stateDb,
moder: newSyncModer(mode, chain, stateDb),
- mux: mux,
queue: newQueue(blockCacheMaxItems, blockCacheInitialItems),
peers: newPeerSet(),
blockchain: chain,
@@ -427,20 +429,25 @@ func (d *Downloader) ConfigSyncMode() SyncMode {
return d.moder.get(false)
}
+// SubscribeSyncEvents creates a subscription for downloader sync events
+func (d *Downloader) SubscribeSyncEvents(ch chan<- SyncEvent) event.Subscription {
+ return d.scope.Track(d.feed.Subscribe(ch))
+}
+
// syncToHead starts a block synchronization based on the hash chain from
// the specified head hash.
func (d *Downloader) syncToHead() (err error) {
- d.mux.Post(StartEvent{})
+ mode := d.getMode()
+ d.feed.Send(SyncEvent{Type: SyncStarted, Mode: mode})
defer func() {
// reset on error
if err != nil {
- d.mux.Post(FailedEvent{err})
+ d.feed.Send(SyncEvent{Type: SyncFailed, Mode: mode, Err: err})
} else {
latest := d.blockchain.CurrentHeader()
- d.mux.Post(DoneEvent{latest})
+ d.feed.Send(SyncEvent{Type: SyncCompleted, Mode: mode, Latest: latest})
}
}()
- mode := d.getMode()
log.Debug("Backfilling with the network", "mode", mode)
defer func(start time.Time) {
@@ -662,6 +669,9 @@ func (d *Downloader) Cancel() {
// Terminate interrupts the downloader, canceling all pending operations.
// The downloader cannot be reused after calling Terminate.
func (d *Downloader) Terminate() {
+ // Unsubscribe all subscriptions registered from downloader
+ d.scope.Close()
+
// Close the termination channel (make sure double close is allowed)
d.quitLock.Lock()
select {
diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go
index 6d5d159631..e6c477cd33 100644
--- a/eth/downloader/downloader_test.go
+++ b/eth/downloader/downloader_test.go
@@ -32,7 +32,6 @@ import (
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/eth/protocols/snap"
- "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
@@ -75,7 +74,7 @@ func newTesterWithNotification(t *testing.T, mode ethconfig.SyncMode, success fu
chain: chain,
peers: make(map[string]*downloadTesterPeer),
}
- tester.downloader = New(db, mode, new(event.TypeMux), tester.chain, tester.dropPeer, success)
+ tester.downloader = New(db, mode, tester.chain, tester.dropPeer, success)
return tester
}
diff --git a/eth/downloader/events.go b/eth/downloader/events.go
index 25255a3a72..0fb380a857 100644
--- a/eth/downloader/events.go
+++ b/eth/downloader/events.go
@@ -16,10 +16,24 @@
package downloader
-import "github.com/ethereum/go-ethereum/core/types"
+import (
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/eth/ethconfig"
+)
-type DoneEvent struct {
- Latest *types.Header
+// SyncEventType represents the type of sync event
+type SyncEventType int
+
+const (
+ SyncStarted SyncEventType = iota
+ SyncFailed
+ SyncCompleted
+)
+
+// SyncEvent represents a downloader synchronization event
+type SyncEvent struct {
+ Type SyncEventType
+ Mode ethconfig.SyncMode
+ Err error // Set when Type is SyncFailed
+ Latest *types.Header // Set when Type is SyncCompleted
}
-type StartEvent struct{}
-type FailedEvent struct{ Err error }
diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go
index dd17b7f1ed..585906b8bd 100644
--- a/eth/downloader/queue.go
+++ b/eth/downloader/queue.go
@@ -689,9 +689,9 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header,
i++
}
- for _, header := range request.Headers[:i] {
+ for k, header := range request.Headers[:i] {
if res, stale, err := q.resultCache.GetDeliverySlot(header.Number.Uint64()); err == nil && !stale {
- reconstruct(accepted, res)
+ reconstruct(k, res)
accepted++
} else {
// Between here and above, some other peer filled this result,
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index dd7436bf52..b51b78e199 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/pathdb"
)
@@ -59,6 +60,7 @@ var Defaults = Config{
StateHistory: pathdb.Defaults.StateHistory,
TrienodeHistory: pathdb.Defaults.TrienodeHistory,
NodeFullValueCheckpoint: pathdb.Defaults.FullValueCheckpoint,
+ BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth,
DatabaseCache: 2048,
TrieCleanCache: 614,
TrieDirtyCache: 1024,
@@ -125,6 +127,11 @@ type Config struct {
// consistent with persistent state.
StateScheme string `toml:",omitempty"`
+ // BinTrieGroupDepth is the number of levels per serialized group in binary trie.
+ // Valid values are 1-8, with 8 being the default (byte-aligned groups).
+ // Lower values create smaller groups with more nodes.
+ BinTrieGroupDepth int `toml:",omitempty"`
+
// RequiredBlocks is a set of block number -> hash mappings which must be in the
// canonical chain of all remote peers. Setting the option makes geth verify the
// presence of these blocks for every new peer connection.
diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go
index ed85562f44..c5e45348be 100644
--- a/eth/ethconfig/gen_config.go
+++ b/eth/ethconfig/gen_config.go
@@ -34,6 +34,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
TrienodeHistory int64 `toml:",omitempty"`
NodeFullValueCheckpoint uint32 `toml:",omitempty"`
StateScheme string `toml:",omitempty"`
+ BinTrieGroupDepth int `toml:",omitempty"`
RequiredBlocks map[uint64]common.Hash `toml:"-"`
SlowBlockThreshold time.Duration `toml:",omitempty"`
SkipBcVersionCheck bool `toml:"-"`
@@ -87,6 +88,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.TrienodeHistory = c.TrienodeHistory
enc.NodeFullValueCheckpoint = c.NodeFullValueCheckpoint
enc.StateScheme = c.StateScheme
+ enc.BinTrieGroupDepth = c.BinTrieGroupDepth
enc.RequiredBlocks = c.RequiredBlocks
enc.SlowBlockThreshold = c.SlowBlockThreshold
enc.SkipBcVersionCheck = c.SkipBcVersionCheck
@@ -144,6 +146,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
TrienodeHistory *int64 `toml:",omitempty"`
NodeFullValueCheckpoint *uint32 `toml:",omitempty"`
StateScheme *string `toml:",omitempty"`
+ BinTrieGroupDepth *int `toml:",omitempty"`
RequiredBlocks map[uint64]common.Hash `toml:"-"`
SlowBlockThreshold *time.Duration `toml:",omitempty"`
SkipBcVersionCheck *bool `toml:"-"`
@@ -234,6 +237,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.StateScheme != nil {
c.StateScheme = *dec.StateScheme
}
+ if dec.BinTrieGroupDepth != nil {
+ c.BinTrieGroupDepth = *dec.BinTrieGroupDepth
+ }
if dec.RequiredBlocks != nil {
c.RequiredBlocks = dec.RequiredBlocks
}
diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go
index ace0752037..f45fc0d8c9 100644
--- a/eth/gasestimator/gasestimator.go
+++ b/eth/gasestimator/gasestimator.go
@@ -22,13 +22,13 @@ import (
"fmt"
"math/big"
- "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
)
// Options are the contextual parameters to execute the requested call.
@@ -70,17 +70,17 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
}
// Normalize the max fee per gas the call is willing to spend.
- var feeCap *big.Int
+ var feeCap *uint256.Int
if call.GasFeeCap != nil {
feeCap = call.GasFeeCap
} else if call.GasPrice != nil {
feeCap = call.GasPrice
} else {
- feeCap = common.Big0
+ feeCap = uint256.NewInt(0)
}
// Recap the highest gas limit with account's available balance.
if feeCap.BitLen() != 0 {
- balance := opts.State.GetBalance(call.From).ToBig()
+ balance := opts.State.GetBalance(call.From).Clone()
available := balance
if call.Value != nil {
@@ -90,8 +90,8 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
available.Sub(available, call.Value)
}
if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 {
- blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob)
- blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes)))
+ blobGasPerBlob := uint256.NewInt(params.BlobTxBlobGasPerBlob)
+ blobBalanceUsage := uint256.NewInt(uint64(len(call.BlobHashes)))
blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob)
blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap)
if blobBalanceUsage.Cmp(available) >= 0 {
@@ -99,13 +99,13 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
}
available.Sub(available, blobBalanceUsage)
}
- allowance := new(big.Int).Div(available, feeCap)
+ allowance := new(uint256.Int).Div(available, feeCap)
// If the allowance is larger than maximum uint64, skip checking
if allowance.IsUint64() && hi > allowance.Uint64() {
transfer := call.Value
if transfer == nil {
- transfer = new(big.Int)
+ transfer = new(uint256.Int)
}
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
diff --git a/eth/handler.go b/eth/handler.go
index 27b5e60697..76df635fb0 100644
--- a/eth/handler.go
+++ b/eth/handler.go
@@ -107,7 +107,6 @@ type handlerConfig struct {
Network uint64 // Network identifier to advertise
Sync ethconfig.SyncMode // Whether to snap or full sync
BloomCache uint64 // Megabytes to alloc for snap sync bloom
- EventMux *event.TypeMux // Legacy event mux, deprecate for `feed`
RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges
}
@@ -126,7 +125,6 @@ type handler struct {
peers *peerSet
txBroadcastKey [16]byte
- eventMux *event.TypeMux
txsCh chan core.NewTxsEvent
txsSub event.Subscription
blockRange *blockRangeState
@@ -144,14 +142,9 @@ type handler struct {
// newHandler returns a handler for all Ethereum chain management protocol.
func newHandler(config *handlerConfig) (*handler, error) {
- // Create the protocol manager with the base fields
- if config.EventMux == nil {
- config.EventMux = new(event.TypeMux) // Nicety initialization for tests
- }
h := &handler{
nodeID: config.NodeID,
networkID: config.Network,
- eventMux: config.EventMux,
database: config.Database,
txpool: config.TxPool,
chain: config.Chain,
@@ -163,7 +156,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
handlerStartCh: make(chan struct{}),
}
// Construct the downloader (long sync)
- h.downloader = downloader.New(config.Database, config.Sync, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures)
+ h.downloader = downloader.New(config.Database, config.Sync, h.chain, h.removePeer, h.enableSyncedFeatures)
// If snap sync is requested but snapshots are disabled, fail loudly
if h.downloader.ConfigSyncMode() == ethconfig.SnapSync && (config.Chain.Snapshots() == nil && config.Chain.TrieDB().Scheme() == rawdb.HashScheme) {
@@ -420,7 +413,7 @@ func (h *handler) Start(maxPeers int) {
// broadcast block range
h.wg.Add(1)
- h.blockRange = newBlockRangeState(h.chain, h.eventMux)
+ h.blockRange = newBlockRangeState(h.chain, h.downloader)
go h.blockRangeLoop(h.blockRange)
// start sync handlers
@@ -536,16 +529,19 @@ type blockRangeState struct {
next atomic.Pointer[eth.BlockRangeUpdatePacket]
headCh chan core.ChainHeadEvent
headSub event.Subscription
- syncSub *event.TypeMuxSubscription
+ syncCh chan downloader.SyncEvent
+ syncSub event.Subscription
}
-func newBlockRangeState(chain *core.BlockChain, typeMux *event.TypeMux) *blockRangeState {
+func newBlockRangeState(chain *core.BlockChain, dl *downloader.Downloader) *blockRangeState {
headCh := make(chan core.ChainHeadEvent, chainHeadChanSize)
headSub := chain.SubscribeChainHeadEvent(headCh)
- syncSub := typeMux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
+ syncCh := make(chan downloader.SyncEvent, 16)
+ syncSub := dl.SubscribeSyncEvents(syncCh)
st := &blockRangeState{
headCh: headCh,
headSub: headSub,
+ syncCh: syncCh,
syncSub: syncSub,
}
st.update(chain, chain.CurrentBlock())
@@ -561,11 +557,8 @@ func (h *handler) blockRangeLoop(st *blockRangeState) {
for {
select {
- case ev := <-st.syncSub.Chan():
- if ev == nil {
- continue
- }
- if _, ok := ev.Data.(downloader.StartEvent); ok && h.downloader.ConfigSyncMode() == ethconfig.SnapSync {
+ case ev := <-st.syncCh:
+ if ev.Type == downloader.SyncStarted && ev.Mode == ethconfig.SnapSync {
h.blockRangeWhileSnapSyncing(st)
}
case <-st.headCh:
@@ -593,12 +586,8 @@ func (h *handler) blockRangeWhileSnapSyncing(st *blockRangeState) {
h.broadcastBlockRange(st)
}
// back to processing head block updates when sync is done
- case ev := <-st.syncSub.Chan():
- if ev == nil {
- continue
- }
- switch ev.Data.(type) {
- case downloader.FailedEvent, downloader.DoneEvent:
+ case ev := <-st.syncCh:
+ if ev.Type == downloader.SyncFailed || ev.Type == downloader.SyncCompleted {
return
}
// ignore head updates, but exit when the subscription ends
diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go
index a45abc90eb..d056d121d9 100644
--- a/eth/protocols/eth/handler_test.go
+++ b/eth/protocols/eth/handler_test.go
@@ -424,16 +424,20 @@ func testGetBlockBodies(t *testing.T, protocol uint) {
{0, []common.Hash{backend.chain.CurrentBlock().Hash()}, []bool{true}, 1}, // The chains head block should be retrievable
{0, []common.Hash{{}}, []bool{false}, 0}, // A non existent block should not be returned
- // Existing and non-existing blocks interleaved should not cause problems
+ // Existing blocks followed by a non-existing one should stop at the gap
+ {0, []common.Hash{
+ backend.chain.GetBlockByNumber(1).Hash(),
+ backend.chain.GetBlockByNumber(10).Hash(),
+ backend.chain.GetBlockByNumber(100).Hash(),
+ {},
+ }, []bool{true, true, true, false}, 3},
+
+ // A non-existing block at the start should return nothing
{0, []common.Hash{
{},
backend.chain.GetBlockByNumber(1).Hash(),
- {},
backend.chain.GetBlockByNumber(10).Hash(),
- {},
- backend.chain.GetBlockByNumber(100).Hash(),
- {},
- }, []bool{false, true, false, true, false, true, false}, 3},
+ }, []bool{false, true, true}, 0},
}
// Run each of the tests and verify the results against the chain
for i, tt := range tests {
diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go
index 7556df9af2..3254a0abc2 100644
--- a/eth/protocols/eth/handlers.go
+++ b/eth/protocols/eth/handlers.go
@@ -238,10 +238,12 @@ func ServiceGetBlockBodiesQuery(chain *core.BlockChain, query GetBlockBodiesRequ
lookups >= 2*maxBodiesServe {
break
}
- if data := chain.GetBodyRLP(hash); len(data) != 0 {
- bodies = append(bodies, data)
- bytes += len(data)
+ data := chain.GetBodyRLP(hash)
+ if len(data) == 0 {
+ break // If we don't have this block's body, stop serving.
}
+ bodies = append(bodies, data)
+ bytes += len(data)
}
return bodies
}
@@ -281,16 +283,16 @@ func ServiceGetReceiptsQuery69(chain *core.BlockChain, query GetReceiptsRequest)
// Retrieve the requested block's receipts
results := chain.GetReceiptsRLP(hash)
if results == nil {
- continue // Can't retrieve the receipts, so we just skip this block.
+ break // Don't have this block's receipts, stop serving.
}
body := chain.GetBodyRLP(hash)
if body == nil {
- continue // The block body is missing, we also have to skip.
+ break // The block body is missing, stop serving.
}
results, _, err := blockReceiptsToNetwork(results, body, receiptQueryParams{})
if err != nil {
log.Error("Error in block receipts conversion", "hash", hash, "err", err)
- continue
+ break
}
receipts.AppendRaw(results)
bytes += len(results)
@@ -312,12 +314,13 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest,
break
}
results := chain.GetReceiptsRLP(hash)
+ // If we don't have this block's receipts or body, stop serving.
if results == nil {
- continue // Can't retrieve the receipts, so we just skip this block.
+ break
}
body := chain.GetBodyRLP(hash)
if body == nil {
- continue // The block body is missing, we also have to skip.
+ break
}
q := receiptQueryParams{sizeLimit: uint64(maxPacketSize - bytes)}
if i == 0 {
@@ -326,7 +329,7 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest,
results, incomplete, err := blockReceiptsToNetwork(results, body, q)
if err != nil {
log.Error("Error in block receipts conversion", "hash", hash, "err", err)
- continue
+ break
}
if results == nil {
// This case triggers when the first receipt of the block receipts list doesn't
diff --git a/eth/state_accessor.go b/eth/state_accessor.go
index a806a4fc56..284ddf4305 100644
--- a/eth/state_accessor.go
+++ b/eth/state_accessor.go
@@ -248,13 +248,10 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil)
evm := vm.NewEVM(context, statedb, eth.blockchain.Config(), vm.Config{})
defer evm.Release()
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- // If prague hardfork, insert parent block hash in the state as per EIP-2935.
- if eth.blockchain.Config().IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+
+ // Run pre-execution system calls
+ core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), eth.blockchain.Config(), evm, block.Number(), block.Time())
+
if txIndex == 0 && len(block.Transactions()) == 0 {
return nil, context, statedb, release, nil
}
@@ -268,7 +265,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
// Not yet the searched for transaction, execute on top of the current state
- statedb.SetTxContext(tx.Hash(), idx)
+ statedb.SetTxContext(tx.Hash(), idx, uint32(idx+1))
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
}
diff --git a/eth/syncer/syncer.go b/eth/syncer/syncer.go
index c0d54b953b..b04d8f22e8 100644
--- a/eth/syncer/syncer.go
+++ b/eth/syncer/syncer.go
@@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth"
+ "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
@@ -37,32 +38,40 @@ type syncReq struct {
errc chan error
}
+type Config struct {
+ TargetBlock common.Hash // if set, sync is triggered at startup
+ ExitWhenSynced bool // if true, the node shuts down after sync has finished
+}
+
// Syncer is an auxiliary service that allows Geth to perform full sync
// alone without consensus-layer attached. Users must specify a valid block hash
// as the sync target.
//
+// Additionally, the syncer can be used to monitor state synchronization.
+// It will exit once the specified target has been reached or when the
+// most recent chain head is caught up.
+//
// This tool can be applied to different networks, no matter it's pre-merge or
// post-merge, but only for full-sync.
type Syncer struct {
- stack *node.Node
- backend *eth.Ethereum
- target common.Hash
- request chan *syncReq
- closed chan struct{}
- wg sync.WaitGroup
- exitWhenSynced bool
+ stack *node.Node
+ backend *eth.Ethereum
+ request chan *syncReq
+ closed chan struct{}
+ wg sync.WaitGroup
+
+ config Config
}
// Register registers the synchronization override service into the node
// stack for launching and stopping the service controlled by node.
-func Register(stack *node.Node, backend *eth.Ethereum, target common.Hash, exitWhenSynced bool) (*Syncer, error) {
+func Register(stack *node.Node, backend *eth.Ethereum, cfg Config) (*Syncer, error) {
s := &Syncer{
- stack: stack,
- backend: backend,
- target: target,
- request: make(chan *syncReq),
- closed: make(chan struct{}),
- exitWhenSynced: exitWhenSynced,
+ stack: stack,
+ backend: backend,
+ request: make(chan *syncReq),
+ closed: make(chan struct{}),
+ config: cfg,
}
stack.RegisterAPIs(s.APIs())
stack.RegisterLifecycle(s)
@@ -88,9 +97,11 @@ func (s *Syncer) run() {
var (
target *types.Header
- ticker = time.NewTicker(time.Second * 5)
+ syncCh = make(chan downloader.SyncEvent, 10)
)
- defer ticker.Stop()
+ sub := s.backend.Downloader().SubscribeSyncEvents(syncCh)
+ defer sub.Unsubscribe()
+
for {
select {
case req := <-s.request:
@@ -137,35 +148,50 @@ func (s *Syncer) run() {
}
}
- case <-ticker.C:
- if target == nil {
+ case ev := <-syncCh:
+ if ev.Type == downloader.SyncStarted {
+ log.Debug("Synchronization started")
continue
}
+ if ev.Type == downloader.SyncFailed {
+ log.Debug("Synchronization failed", "err", ev.Err)
+ continue
+ }
+
+ head := s.backend.BlockChain().CurrentHeader()
+ if head != nil {
+ // Set the finalized and safe markers relative to the current head.
+ // The finalized marker is set two epochs behind the target,
+ // and the safe marker is set one epoch behind the target.
+ if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil {
+ if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 {
+ s.backend.BlockChain().SetFinalized(header)
+ }
+ }
+ if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil {
+ if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 {
+ s.backend.BlockChain().SetSafe(header)
+ }
+ }
+ }
// Terminate the node if the target has been reached
- if s.exitWhenSynced {
- if block := s.backend.BlockChain().GetBlockByHash(target.Hash()); block != nil {
- log.Info("Sync target reached", "number", block.NumberU64(), "hash", block.Hash())
- go s.stack.Close() // async since we need to close ourselves
- return
+ if s.config.ExitWhenSynced {
+ var synced bool
+ var block *types.Header
+ if target != nil {
+ tb := s.backend.BlockChain().GetBlockByHash(target.Hash())
+ synced = tb != nil
+ block = tb.Header()
+ } else {
+ timestamp := time.Unix(int64(ev.Latest.Time), 0)
+ synced = time.Since(timestamp) < 10*time.Minute
+ block = ev.Latest
}
- }
- // Set the finalized and safe markers relative to the current head.
- // The finalized marker is set two epochs behind the target,
- // and the safe marker is set one epoch behind the target.
- head := s.backend.BlockChain().CurrentHeader()
- if head == nil {
- continue
- }
- if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil {
- if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 {
- s.backend.BlockChain().SetFinalized(header)
- }
- }
- if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil {
- if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 {
- s.backend.BlockChain().SetSafe(header)
+ if synced {
+ log.Info("Sync target reached", "number", block.Number.Uint64(), "hash", block.Hash())
+ go s.stack.Close() // async since we need to close ourselves
}
}
@@ -179,10 +205,10 @@ func (s *Syncer) run() {
func (s *Syncer) Start() error {
s.wg.Add(1)
go s.run()
- if s.target == (common.Hash{}) {
+ if s.config.TargetBlock == (common.Hash{}) {
return nil
}
- return s.Sync(s.target)
+ return s.Sync(s.config.TargetBlock)
}
// Stop terminates the synchronization service and stop all background activities.
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index dae11b81de..88132b4b63 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -372,13 +372,8 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
// as per EIP-4788.
context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{})
- if beaconRoot := next.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- // Insert parent hash in history contract.
- if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) {
- core.ProcessParentBlockHash(next.ParentHash(), evm)
- }
+
+ core.PreExecution(ctx, next.BeaconRoot(), next.ParentHash(), api.backend.ChainConfig(), evm, next.Number(), next.Time())
evm.Release()
// Clean out any pending release functions of trace state. Note this
// step must be done after constructing tracing state, because the
@@ -494,8 +489,8 @@ func (api *API) StandardTraceBlockToFile(ctx context.Context, hash common.Hash,
return api.standardTraceBlockToFile(ctx, block, config)
}
-// IntermediateRoots executes a block (bad- or canon- or side-), and returns a list
-// of intermediate roots: the stateroot after each transaction.
+// IntermediateRoots executes a block, and returns a list of intermediate roots:
+// the stateroot after each transaction.
func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config *TraceConfig) ([]common.Hash, error) {
block, _ := api.blockByHash(ctx, hash)
if block == nil {
@@ -517,27 +512,25 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
return nil, err
}
defer release()
+
var (
roots []common.Hash
signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time())
chainConfig = api.backend.ChainConfig()
vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
deleteEmptyObjects = chainConfig.IsEIP158(block.Number())
+ evm = vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{})
)
- evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{})
defer evm.Release()
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if chainConfig.IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+ // Run pre-execution system calls
+ core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time())
+
for i, tx := range block.Transactions() {
if err := ctx.Err(); err != nil {
return nil, err
}
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
- statedb.SetTxContext(tx.Hash(), i)
+ statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err)
// We intentionally don't return the error here: if we do, then the RPC server will not
@@ -548,7 +541,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
// N.B: This should never happen while tracing canon blocks, only when tracing bad blocks.
return roots, nil
}
- // calling IntermediateRoot will internally call Finalize on the state
+ // Calling IntermediateRoot will internally call Finalize on the state
// so any modifications are written to the trie
roots = append(roots, statedb.IntermediateRoot(deleteEmptyObjects))
}
@@ -587,12 +580,9 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{})
defer evm.Release()
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if api.backend.ChainConfig().IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+
+ // Run pre-execution system calls
+ core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), api.backend.ChainConfig(), evm, block.Number(), block.Time())
// JS tracers have high overhead. In this case run a parallel
// process that generates states in one thread and traces txes
@@ -691,7 +681,7 @@ txloop:
// Generate the next state snapshot fast without tracing
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
- statedb.SetTxContext(tx.Hash(), i)
+ statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
if _, err := core.ApplyMessage(evm, msg, nil); err != nil {
failed = err
break txloop
@@ -760,15 +750,12 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
// Note: This copies the config, to not screw up the main config
chainConfig, canon = overrideConfig(chainConfig, config.Overrides)
}
-
evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{})
defer evm.Release()
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if chainConfig.IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+
+ // Run pre-execution system calls
+ core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time())
+
for i, tx := range block.Transactions() {
// Prepare the transaction for un-traced execution
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
@@ -795,6 +782,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
return nil, err
}
dumps = append(dumps, dump.Name())
+
// Set up the tracer and EVM for the transaction.
var (
writer = bufio.NewWriter(dump)
@@ -805,7 +793,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
})
)
// Execute the transaction and flush any traces to disk
- statedb.SetTxContext(tx.Hash(), i)
+ statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
if tracer.OnTxStart != nil {
tracer.OnTxStart(evm.GetVMContext(), tx, msg.From)
}
@@ -1028,9 +1016,9 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
defer cancel()
// Call Prepare to clear out the statedb access list
- statedb.SetTxContext(txctx.TxHash, txctx.TxIndex)
+ statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1))
- _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
+ _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm)
if err != nil {
return nil, fmt.Errorf("tracing failed: %w", err)
}
diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
index 692c5eb775..39067e8efc 100644
--- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go
+++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
@@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) {
}
context := core.NewEVMBlockContext(block.Header(), blockchain, nil)
evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()})
- _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm)
+ _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm)
if err != nil {
t.Fatalf("failed to execute transaction: %v", err)
}
diff --git a/eth/tracers/live/noop.go b/eth/tracers/live/noop.go
index f3def85606..b1784dbd91 100644
--- a/eth/tracers/live/noop.go
+++ b/eth/tracers/live/noop.go
@@ -47,6 +47,7 @@ func newNoopTracer(_ json.RawMessage) (*tracing.Hooks, error) {
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
+ OnGasChangeV2: t.OnGasChangeV2,
OnBlockchainInit: t.OnBlockchainInit,
OnBlockStart: t.OnBlockStart,
OnBlockEnd: t.OnBlockEnd,
@@ -113,3 +114,6 @@ func (t *noop) OnBlockHashRead(number uint64, hash common.Hash) {}
func (t *noop) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
}
+
+func (t *noop) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {
+}
diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go
index 749aade61b..31c3ebde93 100644
--- a/eth/tracers/logger/access_list_tracer.go
+++ b/eth/tracers/logger/access_list_tracer.go
@@ -112,9 +112,10 @@ type AccessListTracer struct {
func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer {
list := newAccessList()
for _, al := range acl {
- if _, ok := addressesToExclude[al.Address]; !ok {
- list.addAddress(al.Address)
+ if _, ok := addressesToExclude[al.Address]; ok {
+ continue
}
+ list.addAddress(al.Address)
for _, slot := range al.StorageKeys {
list.addSlot(al.Address, slot)
}
diff --git a/eth/tracers/logger/access_list_tracer_test.go b/eth/tracers/logger/access_list_tracer_test.go
new file mode 100644
index 0000000000..04b2b4b31b
--- /dev/null
+++ b/eth/tracers/logger/access_list_tracer_test.go
@@ -0,0 +1,39 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package logger
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+func TestNewAccessListTracerExcludedAddress(t *testing.T) {
+ excluded := common.HexToAddress("0x2222222222222222222222222222222222222222")
+ slot := common.HexToHash("0x01")
+ prelude := types.AccessList{{
+ Address: excluded,
+ StorageKeys: []common.Hash{slot},
+ }}
+ excl := map[common.Address]struct{}{excluded: {}}
+ tracer := NewAccessListTracer(prelude, excl)
+ got := tracer.AccessList()
+ if len(got) != 0 {
+ t.Fatalf("excluded prelude address must not contribute tuples, got %+v", got)
+ }
+}
diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go
index 7f2b2aecf2..8e445818ef 100644
--- a/eth/tracers/logger/logger.go
+++ b/eth/tracers/logger/logger.go
@@ -229,9 +229,9 @@ type StructLogger struct {
logs []json.RawMessage // buffer of json-encoded logs
resultSize int
- interrupt atomic.Bool // Atomic flag to signal execution interruption
- reason error // Textual reason for the interruption
- skip bool // skip processing hooks.
+ interrupt atomic.Bool // Atomic flag to signal execution interruption
+ reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
+ skip bool // skip processing hooks.
}
// NewStreamingStructLogger returns a new streaming logger.
@@ -357,8 +357,8 @@ func (l *StructLogger) OnExit(depth int, output []byte, gasUsed uint64, err erro
func (l *StructLogger) GetResult() (json.RawMessage, error) {
// Tracing aborted
- if l.reason != nil {
- return nil, l.reason
+ if p := l.reason.Load(); p != nil {
+ return nil, *p
}
failed := l.err != nil
returnData := common.CopyBytes(l.output)
@@ -376,7 +376,7 @@ func (l *StructLogger) GetResult() (json.RawMessage, error) {
// Stop terminates execution of the tracer at the first opportune moment.
func (l *StructLogger) Stop(err error) {
- l.reason = err
+ l.reason.Store(&err)
l.interrupt.Store(true)
}
diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go
index cec45a1e7a..a542eeffa2 100644
--- a/eth/tracers/native/4byte.go
+++ b/eth/tracers/native/4byte.go
@@ -49,9 +49,9 @@ func init() {
// 0xc281d19e-0: 1
// }
type fourByteTracer struct {
- ids map[string]int // ids aggregates the 4byte ids found
- interrupt atomic.Bool // Atomic flag to signal execution interruption
- reason error // Textual reason for the interruption
+ ids map[string]int // ids aggregates the 4byte ids found
+ interrupt atomic.Bool // Atomic flag to signal execution interruption
+ reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
chainConfig *params.ChainConfig
activePrecompiles []common.Address // Updated on tx start based on given rules
}
@@ -124,12 +124,15 @@ func (t *fourByteTracer) GetResult() (json.RawMessage, error) {
if err != nil {
return nil, err
}
- return res, t.reason
+ if p := t.reason.Load(); p != nil {
+ return res, *p
+ }
+ return res, nil
}
// Stop terminates execution of the tracer at the first opportune moment.
func (t *fourByteTracer) Stop(err error) {
- t.reason = err
+ t.reason.Store(&err)
t.interrupt.Store(true)
}
diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go
index 06220da84d..dfa804827b 100644
--- a/eth/tracers/native/call.go
+++ b/eth/tracers/native/call.go
@@ -116,8 +116,8 @@ type callTracer struct {
config callTracerConfig
gasLimit uint64
depth int
- interrupt atomic.Bool // Atomic flag to signal execution interruption
- reason error // Textual reason for the interruption
+ interrupt atomic.Bool // Atomic flag to signal execution interruption
+ reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
}
type callTracerConfig struct {
@@ -268,12 +268,15 @@ func (t *callTracer) GetResult() (json.RawMessage, error) {
if err != nil {
return nil, err
}
- return res, t.reason
+ if p := t.reason.Load(); p != nil {
+ return res, *p
+ }
+ return res, nil
}
// Stop terminates execution of the tracer at the first opportune moment.
func (t *callTracer) Stop(err error) {
- t.reason = err
+ t.reason.Store(&err)
t.interrupt.Store(true)
}
diff --git a/eth/tracers/native/call_flat.go b/eth/tracers/native/call_flat.go
index 4e7fc31a9c..484f2d4e3b 100644
--- a/eth/tracers/native/call_flat.go
+++ b/eth/tracers/native/call_flat.go
@@ -233,7 +233,10 @@ func (t *flatCallTracer) GetResult() (json.RawMessage, error) {
if err != nil {
return nil, err
}
- return res, t.tracer.reason
+ if p := t.tracer.reason.Load(); p != nil {
+ return res, *p
+ }
+ return res, nil
}
// Stop terminates execution of the tracer at the first opportune moment.
diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go
index 34e202f667..0bf80d77b5 100644
--- a/eth/tracers/native/erc7562.go
+++ b/eth/tracers/native/erc7562.go
@@ -135,8 +135,8 @@ type opcodeWithPartialStack struct {
type erc7562Tracer struct {
config erc7562TracerConfig
gasLimit uint64
- interrupt atomic.Bool // Atomic flag to signal execution interruption
- reason error // Textual reason for the interruption
+ interrupt atomic.Bool // Atomic flag to signal execution interruption
+ reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
env *tracing.VMContext
ignoredOpcodes map[vm.OpCode]struct{}
@@ -317,7 +317,10 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) {
// error arising from the encoding or forceful termination (via `Stop`).
func (t *erc7562Tracer) GetResult() (json.RawMessage, error) {
if t.interrupt.Load() {
- return nil, t.reason
+ if p := t.reason.Load(); p != nil {
+ return nil, *p
+ }
+ return nil, nil
}
if len(t.callstackWithOpcodes) != 1 {
return nil, errors.New("incorrect number of top-level calls")
@@ -337,12 +340,15 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) {
return nil, err
}
- return enc, t.reason
+ if p := t.reason.Load(); p != nil {
+ return enc, *p
+ }
+ return enc, nil
}
// Stop terminates execution of the tracer at the first opportune moment.
func (t *erc7562Tracer) Stop(err error) {
- t.reason = err
+ t.reason.Store(&err)
t.interrupt.Store(true)
}
diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go
index b7d6f29a6a..73f8585a6b 100644
--- a/eth/tracers/native/mux.go
+++ b/eth/tracers/native/mux.go
@@ -63,22 +63,31 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf
//
// The names parameter associates a label with each tracer, used as keys in
// the aggregated JSON result returned by GetResult.
+//
+// For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2,
+// OnNonceChange / OnNonceChangeV2, OnGasChange / OnGasChangeV2,
+// OnSystemCallStart / OnSystemCallStartV2), the mux exposes only the V2
+// variant upward. The fanout then prefers each child's V2 hook and falls
+// back to V1 if only V1 is set, mirroring the precedence already used in
+// core/state_processor.go.
func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) {
t := &muxTracer{names: names, tracers: objects}
return &tracers.Tracer{
Hooks: &tracing.Hooks{
- OnTxStart: t.OnTxStart,
- OnTxEnd: t.OnTxEnd,
- OnEnter: t.OnEnter,
- OnExit: t.OnExit,
- OnOpcode: t.OnOpcode,
- OnFault: t.OnFault,
- OnGasChange: t.OnGasChange,
- OnBalanceChange: t.OnBalanceChange,
- OnNonceChange: t.OnNonceChange,
- OnCodeChange: t.OnCodeChange,
- OnStorageChange: t.OnStorageChange,
- OnLog: t.OnLog,
+ OnTxStart: t.OnTxStart,
+ OnTxEnd: t.OnTxEnd,
+ OnEnter: t.OnEnter,
+ OnExit: t.OnExit,
+ OnOpcode: t.OnOpcode,
+ OnFault: t.OnFault,
+ OnGasChangeV2: t.OnGasChangeV2,
+ OnBalanceChange: t.OnBalanceChange,
+ OnNonceChangeV2: t.OnNonceChangeV2,
+ OnCodeChangeV2: t.OnCodeChangeV2,
+ OnStorageChange: t.OnStorageChange,
+ OnLog: t.OnLog,
+ OnSystemCallStartV2: t.OnSystemCallStart,
+ OnSystemCallEnd: t.OnSystemCallEnd,
},
GetResult: t.GetResult,
Stop: t.Stop,
@@ -101,10 +110,12 @@ func (t *muxTracer) OnFault(pc uint64, op byte, gas, cost uint64, scope tracing.
}
}
-func (t *muxTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
+func (t *muxTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {
for _, t := range t.tracers {
- if t.OnGasChange != nil {
- t.OnGasChange(old, new, reason)
+ if t.OnGasChangeV2 != nil {
+ t.OnGasChangeV2(old, new, reason)
+ } else if t.OnGasChange != nil {
+ t.OnGasChange(old.Regular, new.Regular, reason)
}
}
}
@@ -149,26 +160,22 @@ func (t *muxTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason
}
}
-func (t *muxTracer) OnNonceChange(a common.Address, prev, new uint64) {
+func (t *muxTracer) OnNonceChangeV2(a common.Address, prev, new uint64, reason tracing.NonceChangeReason) {
for _, t := range t.tracers {
- if t.OnNonceChange != nil {
+ if t.OnNonceChangeV2 != nil {
+ t.OnNonceChangeV2(a, prev, new, reason)
+ } else if t.OnNonceChange != nil {
t.OnNonceChange(a, prev, new)
}
}
}
-func (t *muxTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) {
- for _, t := range t.tracers {
- if t.OnCodeChange != nil {
- t.OnCodeChange(a, prevCodeHash, prev, codeHash, code)
- }
- }
-}
-
func (t *muxTracer) OnCodeChangeV2(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) {
for _, t := range t.tracers {
if t.OnCodeChangeV2 != nil {
t.OnCodeChangeV2(a, prevCodeHash, prev, codeHash, code, reason)
+ } else if t.OnCodeChange != nil {
+ t.OnCodeChange(a, prevCodeHash, prev, codeHash, code)
}
}
}
@@ -189,6 +196,24 @@ func (t *muxTracer) OnLog(log *types.Log) {
}
}
+func (t *muxTracer) OnSystemCallStart(vm *tracing.VMContext) {
+ for _, t := range t.tracers {
+ if t.OnSystemCallStartV2 != nil {
+ t.OnSystemCallStartV2(vm)
+ } else if t.OnSystemCallStart != nil {
+ t.OnSystemCallStart()
+ }
+ }
+}
+
+func (t *muxTracer) OnSystemCallEnd() {
+ for _, t := range t.tracers {
+ if t.OnSystemCallEnd != nil {
+ t.OnSystemCallEnd()
+ }
+ }
+}
+
// GetResult returns an empty json object.
func (t *muxTracer) GetResult() (json.RawMessage, error) {
resObject := make(map[string]json.RawMessage)
diff --git a/eth/tracers/native/mux_test.go b/eth/tracers/native/mux_test.go
new file mode 100644
index 0000000000..902b7a026a
--- /dev/null
+++ b/eth/tracers/native/mux_test.go
@@ -0,0 +1,87 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package native
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/eth/tracers"
+)
+
+// TestMuxForwardsV2StateHooks verifies that the mux tracer fans out the V2
+// variants of state-change hooks to child tracers. A child tracer that only
+// implements OnCodeChangeV2 / OnNonceChangeV2 must still receive events when
+// wrapped behind the mux. The mux must also fall back to the V1 hook when a
+// child only implements V1, mirroring the precedence used in
+// core/state_processor.go.
+func TestMuxForwardsV2StateHooks(t *testing.T) {
+ var (
+ codeV2Calls int
+ nonceV2Calls int
+ codeV1Calls int
+ nonceV1Calls int
+ )
+ v2Child := &tracers.Tracer{
+ Hooks: &tracing.Hooks{
+ OnCodeChangeV2: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) {
+ codeV2Calls++
+ },
+ OnNonceChangeV2: func(addr common.Address, prev, new uint64, reason tracing.NonceChangeReason) {
+ nonceV2Calls++
+ },
+ },
+ }
+ v1Child := &tracers.Tracer{
+ Hooks: &tracing.Hooks{
+ OnCodeChange: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) {
+ codeV1Calls++
+ },
+ OnNonceChange: func(addr common.Address, prev, new uint64) {
+ nonceV1Calls++
+ },
+ },
+ }
+ mux, err := NewMuxTracer([]string{"v2", "v1"}, []*tracers.Tracer{v2Child, v1Child})
+ if err != nil {
+ t.Fatalf("NewMuxTracer: %v", err)
+ }
+
+ if mux.Hooks.OnCodeChangeV2 == nil {
+ t.Fatal("mux does not expose OnCodeChangeV2; V2-only child tracers will miss code changes")
+ }
+ if mux.Hooks.OnNonceChangeV2 == nil {
+ t.Fatal("mux does not expose OnNonceChangeV2; V2-only child tracers will miss nonce changes")
+ }
+
+ mux.Hooks.OnCodeChangeV2(common.Address{}, common.Hash{}, nil, common.Hash{}, nil, tracing.CodeChangeContractCreation)
+ mux.Hooks.OnNonceChangeV2(common.Address{}, 0, 1, tracing.NonceChangeEoACall)
+
+ if codeV2Calls != 1 {
+ t.Fatalf("V2 child OnCodeChangeV2 got %d calls, want 1", codeV2Calls)
+ }
+ if nonceV2Calls != 1 {
+ t.Fatalf("V2 child OnNonceChangeV2 got %d calls, want 1", nonceV2Calls)
+ }
+ if codeV1Calls != 1 {
+ t.Fatalf("V1 child OnCodeChange got %d calls, want 1 (mux should fall back from V2 to V1)", codeV1Calls)
+ }
+ if nonceV1Calls != 1 {
+ t.Fatalf("V1 child OnNonceChange got %d calls, want 1 (mux should fall back from V2 to V1)", nonceV1Calls)
+ }
+}
diff --git a/eth/tracers/native/noop.go b/eth/tracers/native/noop.go
index ac174cc25e..323bf4338f 100644
--- a/eth/tracers/native/noop.go
+++ b/eth/tracers/native/noop.go
@@ -47,6 +47,7 @@ func newNoopTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *param
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
+ OnGasChangeV2: t.OnGasChangeV2,
OnBalanceChange: t.OnBalanceChange,
OnNonceChange: t.OnNonceChange,
OnCodeChange: t.OnCodeChange,
@@ -66,6 +67,8 @@ func (t *noopTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpC
func (t *noopTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {}
+func (t *noopTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {}
+
func (t *noopTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
}
diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go
index 36cb16e44b..7026cca7f3 100644
--- a/eth/tracers/native/prestate.go
+++ b/eth/tracers/native/prestate.go
@@ -71,8 +71,8 @@ type prestateTracer struct {
to common.Address
config PrestateTracerConfig
chainConfig *params.ChainConfig
- interrupt atomic.Bool // Atomic flag to signal execution interruption
- reason error // Textual reason for the interruption
+ interrupt atomic.Bool // Atomic flag to signal execution interruption
+ reason atomic.Pointer[error] // Reason for the interruption, populated by Stop
created map[common.Address]bool
deleted map[common.Address]bool
}
@@ -240,12 +240,15 @@ func (t *prestateTracer) GetResult() (json.RawMessage, error) {
if err != nil {
return nil, err
}
- return json.RawMessage(res), t.reason
+ if p := t.reason.Load(); p != nil {
+ return json.RawMessage(res), *p
+ }
+ return json.RawMessage(res), nil
}
// Stop terminates execution of the tracer at the first opportune moment.
func (t *prestateTracer) Stop(err error) {
- t.reason = err
+ t.reason.Store(&err)
t.interrupt.Store(true)
}
diff --git a/eth/tracers/native/tracer_test.go b/eth/tracers/native/tracer_test.go
new file mode 100644
index 0000000000..70e6283d34
--- /dev/null
+++ b/eth/tracers/native/tracer_test.go
@@ -0,0 +1,80 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package native_test
+
+import (
+ "errors"
+ "math/big"
+ "sync"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/eth/tracers"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/stretchr/testify/require"
+)
+
+// TestTracerStopRace exercises the concurrent Stop / GetResult path that the
+// trace RPC handler uses: a timeout watchdog goroutine calls Stop while the
+// main goroutine is still running the trace and will eventually call
+// GetResult. Under -race, writes to the interruption reason field must not
+// race with reads, for every tracer that implements it.
+//
+// callTracer, flatCallTracer and erc7562Tracer's GetResult short-circuits on
+// an empty callstack ("incorrect number of top-level calls") before loading
+// the reason. For those tracers the test pushes a single top-level call frame
+// via OnEnter so GetResult reaches the reason.Load() path where the race can
+// be observed under -race.
+func TestTracerStopRace(t *testing.T) {
+ type setup struct {
+ name string
+ needsFrame bool // whether GetResult requires a top-level call frame
+ }
+ cases := []setup{
+ {"callTracer", true},
+ {"flatCallTracer", true},
+ {"4byteTracer", false},
+ {"prestateTracer", false},
+ {"erc7562Tracer", true},
+ }
+ for _, s := range cases {
+ t.Run(s.name, func(t *testing.T) {
+ tr, err := tracers.DefaultDirectory.New(s.name, &tracers.Context{}, nil, params.MainnetChainConfig)
+ require.NoError(t, err)
+
+ if s.needsFrame && tr.OnEnter != nil {
+ // Push a single top-level call frame so GetResult doesn't
+ // short-circuit before reading the interruption reason.
+ tr.OnEnter(0, byte(vm.CALL), common.Address{}, common.Address{}, nil, 0, big.NewInt(0))
+ }
+
+ stopErr := errors.New("execution timeout")
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ tr.Stop(stopErr)
+ }()
+ go func() {
+ defer wg.Done()
+ _, _ = tr.GetResult()
+ }()
+ wg.Wait()
+ })
+ }
+}
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 412f8955ba..1d8573f982 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -914,6 +914,7 @@ type SimulateCallResult struct {
ReturnValue []byte `json:"returnData"`
Logs []*types.Log `json:"logs"`
GasUsed uint64 `json:"gasUsed"`
+ MaxUsedGas uint64 `json:"maxUsedGas"`
Status uint64 `json:"status"`
Error *CallError `json:"error,omitempty"`
}
@@ -921,6 +922,7 @@ type SimulateCallResult struct {
type simulateCallResultMarshaling struct {
ReturnValue hexutil.Bytes
GasUsed hexutil.Uint64
+ MaxUsedGas hexutil.Uint64
Status hexutil.Uint64
}
diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go
index f9e761e412..fb04d77669 100644
--- a/ethclient/ethclient_test.go
+++ b/ethclient/ethclient_test.go
@@ -861,6 +861,12 @@ func TestSimulateV1(t *testing.T) {
if results[0].Calls[0].Error != nil {
t.Errorf("expected no error, got %v", results[0].Calls[0].Error)
}
+ if results[0].Calls[0].MaxUsedGas == 0 {
+ t.Error("expected maxUsedGas to be set")
+ }
+ if results[0].Calls[0].MaxUsedGas < results[0].Calls[0].GasUsed {
+ t.Errorf("expected maxUsedGas >= gasUsed, got %d < %d", results[0].Calls[0].MaxUsedGas, results[0].Calls[0].GasUsed)
+ }
}
func TestSimulateV1WithBlockOverrides(t *testing.T) {
diff --git a/ethclient/gen_simulate_call_result.go b/ethclient/gen_simulate_call_result.go
index 55e14cd697..18373bbb88 100644
--- a/ethclient/gen_simulate_call_result.go
+++ b/ethclient/gen_simulate_call_result.go
@@ -17,6 +17,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) {
ReturnValue hexutil.Bytes `json:"returnData"`
Logs []*types.Log `json:"logs"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
+ MaxUsedGas hexutil.Uint64 `json:"maxUsedGas"`
Status hexutil.Uint64 `json:"status"`
Error *CallError `json:"error,omitempty"`
}
@@ -24,6 +25,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) {
enc.ReturnValue = s.ReturnValue
enc.Logs = s.Logs
enc.GasUsed = hexutil.Uint64(s.GasUsed)
+ enc.MaxUsedGas = hexutil.Uint64(s.MaxUsedGas)
enc.Status = hexutil.Uint64(s.Status)
enc.Error = s.Error
return json.Marshal(&enc)
@@ -35,6 +37,7 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error {
ReturnValue *hexutil.Bytes `json:"returnData"`
Logs []*types.Log `json:"logs"`
GasUsed *hexutil.Uint64 `json:"gasUsed"`
+ MaxUsedGas *hexutil.Uint64 `json:"maxUsedGas"`
Status *hexutil.Uint64 `json:"status"`
Error *CallError `json:"error,omitempty"`
}
@@ -51,6 +54,9 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error {
if dec.GasUsed != nil {
s.GasUsed = uint64(*dec.GasUsed)
}
+ if dec.MaxUsedGas != nil {
+ s.MaxUsedGas = uint64(*dec.MaxUsedGas)
+ }
if dec.Status != nil {
s.Status = uint64(*dec.Status)
}
diff --git a/ethclient/simulated/backend.go b/ethclient/simulated/backend.go
index d573c7e750..160ad924bf 100644
--- a/ethclient/simulated/backend.go
+++ b/ethclient/simulated/backend.go
@@ -86,6 +86,8 @@ func NewBackend(alloc types.GenesisAlloc, options ...func(nodeConf *node.Config,
}
ethConf.SyncMode = ethconfig.FullSync
ethConf.TxPool.NoLocals = true
+ // Disable log indexing to force unindexed log search
+ ethConf.LogNoHistory = true
for _, option := range options {
option(&nodeConf, ðConf)
diff --git a/internal/build/gotool.go b/internal/build/gotool.go
index 172fa13464..00aa9d6f02 100644
--- a/internal/build/gotool.go
+++ b/internal/build/gotool.go
@@ -41,12 +41,19 @@ type GoToolchain struct {
func (g *GoToolchain) Go(command string, args ...string) *exec.Cmd {
tool := g.goTool(command, args...)
- // Configure environment for cross build.
- if g.GOARCH != "" && g.GOARCH != runtime.GOARCH {
+ // Configure environment for cross build. Force CGO_ENABLED=1 whenever
+ // either GOOS or GOARCH differs from the host: Go's default is
+ // CGO_ENABLED=0 for any cross-compile, but geth's release builds rely
+ // on cgo (c-kzg-4844, secp256k1) regardless of which axis is crossing.
+ crossArch := g.GOARCH != "" && g.GOARCH != runtime.GOARCH
+ crossOS := g.GOOS != "" && g.GOOS != runtime.GOOS
+ if crossArch || crossOS {
tool.Env = append(tool.Env, "CGO_ENABLED=1")
+ }
+ if crossArch {
tool.Env = append(tool.Env, "GOARCH="+g.GOARCH)
}
- if g.GOOS != "" && g.GOOS != runtime.GOOS {
+ if crossOS {
tool.Env = append(tool.Env, "GOOS="+g.GOOS)
}
// Configure C compiler.
diff --git a/internal/download/download.go b/internal/download/download.go
index c59c8a90c3..94517166f5 100644
--- a/internal/download/download.go
+++ b/internal/download/download.go
@@ -22,6 +22,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/hex"
+ "errors"
"fmt"
"io"
"iter"
@@ -180,12 +181,13 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error {
return fmt.Errorf("no known hash for file %q", basename)
}
// Shortcut if already downloaded.
- if verifyHash(dstPath, hash) == nil {
+ if err := verifyHash(dstPath, hash); err == nil {
fmt.Printf("%s is up-to-date\n", dstPath)
return nil
+ } else if !errors.Is(err, os.ErrNotExist) {
+ fmt.Printf("%s is stale\n", dstPath)
}
- fmt.Printf("%s is stale\n", dstPath)
fmt.Printf("downloading from %s\n", url)
resp, err := http.Get(url)
if err != nil {
@@ -209,9 +211,12 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error {
if resp.ContentLength > 0 {
dst = newDownloadWriter(fd, resp.ContentLength)
}
- _, err = io.Copy(dst, resp.Body)
- dst.Close()
- if err != nil {
+ if _, err = io.Copy(dst, resp.Body); err != nil {
+ dst.Close()
+ os.Remove(tmpfile)
+ return err
+ }
+ if err = dst.Close(); err != nil {
os.Remove(tmpfile)
return err
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index e8669b86c6..109169e0b0 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -146,6 +146,11 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big {
return (*hexutil.Big)(api.b.BlobBaseFee(ctx))
}
+// BaseFee returns the base fee for the next block.
+func (api *EthereumAPI) BaseFee(ctx context.Context) *hexutil.Big {
+ return (*hexutil.Big)(api.b.BaseFee(ctx))
+}
+
// Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not
// yet received the latest block headers from its peers. In case it is synchronizing:
// - startingBlock: block number this node started to synchronize from
@@ -734,6 +739,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
if err := blockOverrides.Apply(&blockCtx); err != nil {
return nil, err
}
+ // Override the header so callers that compute gas price from 1559 fee
+ // fields see the overridden basefee. Otherwise GASPRICE/effectiveTip
+ // would be derived from the pre-override basefee.
+ header = blockOverrides.MakeHeader(header)
}
rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time)
precompiles := vm.ActivePrecompiledContracts(rules)
@@ -992,6 +1001,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} {
if head.RequestsHash != nil {
result["requestsHash"] = head.RequestsHash
}
+ if head.BlockAccessListHash != nil {
+ result["balHash"] = head.BlockAccessListHash
+ }
if head.SlotNumber != nil {
result["slotNumber"] = hexutil.Uint64(*head.SlotNumber)
}
@@ -2119,8 +2131,7 @@ func (api *DebugAPI) SetHead(number hexutil.Uint64) error {
if header.Number.Uint64() <= uint64(number) {
return errors.New("not allowed to rewind to a future block")
}
- api.b.SetHead(uint64(number))
- return nil
+ return api.b.SetHead(uint64(number))
}
// NetAPI offers network related RPC methods
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index 6cf52d636a..561ce2c2d2 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -500,6 +500,7 @@ func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBloc
return nil, nil, nil, nil, nil, nil, nil
}
func (b testBackend) BlobBaseFee(ctx context.Context) *big.Int { return new(big.Int) }
+func (b testBackend) BaseFee(ctx context.Context) *big.Int { return new(big.Int) }
func (b testBackend) ChainDb() ethdb.Database { return b.db }
func (b testBackend) AccountManager() *accounts.Manager { return b.accman }
func (b testBackend) ExtRPCEnabled() bool { return false }
@@ -507,7 +508,7 @@ func (b testBackend) RPCGasCap() uint64 { return 10000000
func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second }
func (b testBackend) RPCTxFeeCap() float64 { return 0 }
func (b testBackend) UnprotectedAllowed() bool { return false }
-func (b testBackend) SetHead(number uint64) {}
+func (b testBackend) SetHead(number uint64) error { return nil }
func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
if number == rpc.LatestBlockNumber {
return b.chain.CurrentBlock(), nil
@@ -1315,6 +1316,27 @@ func TestCall(t *testing.T) {
},
expectErr: errors.New(`block override "withdrawals" is not supported for this RPC method`),
},
+ // Verify that an overridden basefee is honored when computing gasPrice
+ // from the 1559 fee fields. Returning GASPRICE opcode; expected value
+ // is min(MaxFeePerGas, MaxPriorityFeePerGas + overridden BaseFee).
+ //
+ // BaseFee override = 0xa (10); MaxFeePerGas = 0x64 (100);
+ // MaxPriorityFeePerGas = 0x2 (2); expected GASPRICE = 12.
+ {
+ name: "basefee-override-used-in-gasprice",
+ blockNumber: rpc.LatestBlockNumber,
+ call: TransactionArgs{
+ From: &accounts[0].addr,
+ // Contract: GASPRICE; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN
+ Input: hex2Bytes("3a60005260206000f3"),
+ MaxFeePerGas: (*hexutil.Big)(big.NewInt(100)),
+ MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(2)),
+ },
+ blockOverrides: override.BlockOverrides{
+ BaseFeePerGas: (*hexutil.Big)(big.NewInt(10)),
+ },
+ want: "0x000000000000000000000000000000000000000000000000000000000000000c",
+ },
}
for _, tc := range testSuite {
result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides)
@@ -2659,6 +2681,67 @@ func TestSimulateV1TxSender(t *testing.T) {
require.Equal(t, sender2, summary[1].Transactions[0].From, "sender address mismatch")
}
+// TestSimulateV1WithdrawalsByFork verifies that withdrawals and withdrawalsRoot
+// are only emitted in the simulated block result when the simulated block is
+// post-Shanghai. Pre-Shanghai blocks must omit both fields, otherwise the
+// header hash and size would not match a valid pre-Shanghai block.
+func TestSimulateV1WithdrawalsByFork(t *testing.T) {
+ t.Parallel()
+
+ run := func(t *testing.T, cfg *params.ChainConfig, blockTime *uint64, wantWithdrawals bool) {
+ t.Helper()
+ gspec := &core.Genesis{Config: cfg, Alloc: types.GenesisAlloc{}}
+ backend := newTestBackend(t, 1, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {})
+
+ ctx := context.Background()
+ stateDB, baseHeader, err := backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
+ if err != nil {
+ t.Fatalf("failed to get state and header: %v", err)
+ }
+ sim := &simulator{
+ b: backend,
+ state: stateDB,
+ base: baseHeader,
+ chainConfig: backend.ChainConfig(),
+ budget: newGasBudget(0),
+ }
+
+ block := simBlock{}
+ if blockTime != nil {
+ t := hexutil.Uint64(*blockTime)
+ block.BlockOverrides = &override.BlockOverrides{Time: &t}
+ }
+ results, err := sim.execute(ctx, []simBlock{block})
+ if err != nil {
+ t.Fatalf("simulation execution failed: %v", err)
+ }
+ require.Len(t, results, 1)
+
+ enc, err := json.Marshal(results[0])
+ if err != nil {
+ t.Fatalf("failed to marshal result: %v", err)
+ }
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(enc, &raw); err != nil {
+ t.Fatalf("failed to unmarshal result: %v", err)
+ }
+ _, hasWithdrawals := raw["withdrawals"]
+ _, hasWithdrawalsRoot := raw["withdrawalsRoot"]
+ if hasWithdrawals != wantWithdrawals || hasWithdrawalsRoot != wantWithdrawals {
+ t.Fatalf("unexpected withdrawals fields: withdrawals=%v withdrawalsRoot=%v want=%v\n%s", hasWithdrawals, hasWithdrawalsRoot, wantWithdrawals, enc)
+ }
+ }
+
+ t.Run("pre-shanghai", func(t *testing.T) {
+ // TestChainConfig has ShanghaiTime=nil, so all simulated blocks are pre-Shanghai.
+ run(t, params.TestChainConfig, nil, false)
+ })
+ t.Run("post-shanghai", func(t *testing.T) {
+ // MergedTestChainConfig has every fork active from genesis.
+ run(t, params.MergedTestChainConfig, nil, true)
+ })
+}
+
func TestSignTransaction(t *testing.T) {
t.Parallel()
// Initialize test accounts
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index af3d592b82..f23be85782 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -46,6 +46,7 @@ type Backend interface {
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error)
BlobBaseFee(ctx context.Context) *big.Int
+ BaseFee(ctx context.Context) *big.Int
ChainDb() ethdb.Database
AccountManager() *accounts.Manager
ExtRPCEnabled() bool
@@ -57,7 +58,7 @@ type Backend interface {
RPCTxSyncMaxTimeout() time.Duration
// Blockchain API
- SetHead(number uint64)
+ SetHead(number uint64) error
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error)
diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go
index e3a14bf5d6..8462194b1d 100644
--- a/internal/ethapi/simulate.go
+++ b/internal/ethapi/simulate.go
@@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/ethapi/override"
"github.com/ethereum/go-ethereum/params"
@@ -292,9 +293,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
gp = core.NewGasPool(blockContext.GasLimit)
blobGasUsed uint64
- txes = make([]*types.Transaction, len(block.Calls))
- callResults = make([]simCallResult, len(block.Calls))
- receipts = make([]*types.Receipt, len(block.Calls))
+ txes = make([]*types.Transaction, len(block.Calls))
+ callResults = make([]simCallResult, len(block.Calls))
+ receipts = make([]*types.Receipt, len(block.Calls))
+ blockAccessList = bal.NewConstructionBlockAccessList()
// Block hash will be repaired after execution.
tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0)
@@ -313,17 +315,15 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig)
defer evm.Release()
+
// It is possible to override precompiles with EVM bytecode, or
// move them to another address.
if precompiles != nil {
evm.SetPrecompiles(precompiles)
}
- if sim.chainConfig.IsPrague(header.Number, header.Time) || sim.chainConfig.IsUBT(header.Number, header.Time) {
- core.ProcessParentBlockHash(header.ParentHash, evm)
- }
- if header.ParentBeaconRoot != nil {
- core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, evm)
- }
+ // Run pre-execution system calls
+ blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time))
+
var allLogs []*types.Log
for i, call := range block.Calls {
// Terminate if the context is cancelled
@@ -343,7 +343,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
tracer.reset(txHash, uint(i))
// EoA check is always skipped, even in validation mode.
- sim.state.SetTxContext(txHash, i)
+ sim.state.SetTxContext(txHash, i, uint32(i+1))
msg := call.ToMessage(header.BaseFee, !sim.validate)
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp)
if err != nil {
@@ -353,7 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
// Update the state with pending changes.
var root []byte
if sim.chainConfig.IsByzantium(blockContext.BlockNumber) {
- tracingStateDB.Finalise(true)
+ blockAccessList.Merge(tracingStateDB.Finalise(true))
} else {
root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes()
}
@@ -394,35 +394,32 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
// Process EIP-7685 requests
- var requests [][]byte
- if sim.chainConfig.IsPrague(header.Number, header.Time) {
- requests = [][]byte{}
- // EIP-6110
- if err := core.ParseDepositLogs(&requests, allLogs, sim.chainConfig); err != nil {
- return nil, nil, nil, err
- }
- // EIP-7002
- if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil {
- return nil, nil, nil, err
- }
- // EIP-7251
- if err := core.ProcessConsolidationQueue(&requests, evm); err != nil {
- return nil, nil, nil, err
- }
+ requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1))
+ if err != nil {
+ return nil, nil, nil, err
}
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
header.RequestsHash = &reqHash
}
+ blockAccessList.Merge(bal)
blockBody := &types.Body{
Transactions: txes,
- Withdrawals: *block.BlockOverrides.Withdrawals, // Withdrawal is also sanitized as non-nil
+ }
+ // Withdrawals are a post-Shanghai field. Attaching a non-nil withdrawals
+ // slice would cause types.NewBlock to populate WithdrawalsHash on the
+ // header and emit withdrawals fields for pre-Shanghai blocks.
+ if sim.chainConfig.IsShanghai(header.Number, header.Time) {
+ blockBody.Withdrawals = *block.BlockOverrides.Withdrawals
}
chainHeadReader := &simChainHeadReader{ctx, sim.b}
+ // Apply the consensus-specific post-transaction changes
+ sim.b.Engine().Finalize(chainHeadReader, header, sim.state, blockBody, uint32(len(block.Calls)+1), blockAccessList)
+
// Assemble the block
- b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts)
+ b := core.AssembleBlock(chainHeadReader, header, sim.state, blockBody, receipts, blockAccessList)
repairLogs(callResults, b.Hash())
return b, callResults, senders, nil
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 4fb30e6289..1032d067f1 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -446,27 +446,27 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int,
// Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called.
func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *core.Message {
var (
- gasPrice *big.Int
- gasFeeCap *big.Int
- gasTipCap *big.Int
+ gasPrice *uint256.Int
+ gasFeeCap *uint256.Int
+ gasTipCap *uint256.Int
)
if baseFee == nil {
- gasPrice = args.GasPrice.ToInt()
+ gasPrice, _ = args.GasPrice.ToUint256()
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// A basefee is provided, necessitating 1559-type execution
if args.GasPrice != nil {
// User specified the legacy gas field, convert to 1559 gas typing
- gasPrice = args.GasPrice.ToInt()
+ gasPrice, _ = args.GasPrice.ToUint256()
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// User specified 1559 gas fields (or none), use those
- gasFeeCap = args.MaxFeePerGas.ToInt()
- gasTipCap = args.MaxPriorityFeePerGas.ToInt()
+ gasFeeCap, _ = args.MaxFeePerGas.ToUint256()
+ gasTipCap, _ = args.MaxPriorityFeePerGas.ToUint256()
// Backfill the legacy gasPrice for EVM execution, unless we're all zeroes
- gasPrice = new(big.Int)
+ gasPrice = uint256.NewInt(0)
if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 {
- gasPrice = gasPrice.Add(gasTipCap, baseFee)
+ gasPrice = gasPrice.Add(gasTipCap, uint256.MustFromBig(baseFee))
if gasPrice.Cmp(gasFeeCap) > 0 {
gasPrice = gasFeeCap
}
@@ -477,10 +477,12 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c
if args.AccessList != nil {
accessList = *args.AccessList
}
+ value, _ := args.Value.ToUint256()
+ blobFeeCap, _ := args.BlobFeeCap.ToUint256()
return &core.Message{
From: args.from(),
To: args.To,
- Value: (*big.Int)(args.Value),
+ Value: value,
Nonce: uint64(*args.Nonce),
GasLimit: uint64(*args.Gas),
GasPrice: gasPrice,
@@ -488,7 +490,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c
GasTipCap: gasTipCap,
Data: args.data(),
AccessList: accessList,
- BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
+ BlobGasFeeCap: blobFeeCap,
BlobHashes: args.BlobHashes,
SetCodeAuthorizations: args.AuthorizationList,
SkipNonceChecks: skipNonceCheck,
diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go
index 30791f32b5..ccb46a810d 100644
--- a/internal/ethapi/transaction_args_test.go
+++ b/internal/ethapi/transaction_args_test.go
@@ -318,6 +318,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return big.NewInt(42), nil
}
func (b *backendMock) BlobBaseFee(ctx context.Context) *big.Int { return big.NewInt(42) }
+func (b *backendMock) BaseFee(ctx context.Context) *big.Int { return big.NewInt(42) }
func (b *backendMock) CurrentHeader() *types.Header { return b.current }
func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config }
@@ -336,7 +337,7 @@ func (b *backendMock) RPCGasCap() uint64 { return 0 }
func (b *backendMock) RPCEVMTimeout() time.Duration { return time.Second }
func (b *backendMock) RPCTxFeeCap() float64 { return 0 }
func (b *backendMock) UnprotectedAllowed() bool { return false }
-func (b *backendMock) SetHead(number uint64) {}
+func (b *backendMock) SetHead(number uint64) error { return nil }
func (b *backendMock) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
return nil, nil
}
diff --git a/miner/worker.go b/miner/worker.go
index 42e3695025..21bc95cf92 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/log"
@@ -71,6 +72,7 @@ type environment struct {
receipts []*types.Receipt
sidecars []*types.BlobTxSidecar
blobs int
+ bal *bal.ConstructionBlockAccessList
witness *stateless.Witness
}
@@ -167,7 +169,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
// otherwise, fill the block with the current transactions from the txpool
if genParam.forceOverrides && len(genParam.overrideTxs) > 0 {
for _, tx := range genParam.overrideTxs {
- work.state.SetTxContext(tx.Hash(), work.tcount)
+ work.state.SetTxContext(tx.Hash(), work.tcount, uint32(work.tcount+1))
if err := miner.commitTransaction(ctx, work, tx); err != nil {
// all passed transactions HAVE to be valid at this point
return &newPayloadResult{err: err}
@@ -208,29 +210,22 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
}
// Collect consensus-layer requests if Prague is enabled.
- var requests [][]byte
- if miner.chainConfig.IsPrague(work.header.Number, work.header.Time) {
- requests = [][]byte{}
- // EIP-6110 deposits
- if err := core.ParseDepositLogs(&requests, allLogs, miner.chainConfig); err != nil {
- return &newPayloadResult{err: err}
- }
- // EIP-7002
- if err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil {
- return &newPayloadResult{err: err}
- }
- // EIP-7251 consolidations
- if err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil {
- return &newPayloadResult{err: err}
- }
+ requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1))
+ if err != nil {
+ return &newPayloadResult{err: err}
}
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
work.header.RequestsHash = &reqHash
}
+ work.bal.Merge(bal)
+
+ // Apply the consensus-specific post-transaction changes
+ miner.engine.Finalize(miner.chain, work.header, work.state, &body, uint32(work.tcount+1), work.bal)
+
// Assemble the block for delivery.
_, _, assembleSpanEnd := telemetry.StartSpan(ctx, "miner.AssembleBlock")
- block := core.AssembleBlock(miner.engine, miner.chain, work.header, work.state, &body, work.receipts)
+ block := core.AssembleBlock(miner.chain, work.header, work.state, &body, work.receipts, work.bal)
assembleSpanEnd(nil)
return &newPayloadResult{
@@ -329,12 +324,8 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams,
log.Error("Failed to create sealing context", "err", err)
return nil, err
}
- if header.ParentBeaconRoot != nil {
- core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm)
- }
- if miner.chainConfig.IsPrague(header.Number, header.Time) {
- core.ProcessParentBlockHash(header.ParentHash, env.evm)
- }
+ // Run pre-execution system calls
+ env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time))
return env, nil
}
@@ -353,6 +344,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
}
}
state.StartPrefetcher("miner", bundle)
+
// Note the passed coinbase may be different with header.Coinbase.
return &environment{
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),
@@ -361,6 +353,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
coinbase: coinbase,
gasPool: core.NewGasPool(header.GasLimit),
header: header,
+ bal: bal.NewConstructionBlockAccessList(),
witness: state.Witness(),
evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}),
}, nil
@@ -372,7 +365,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx
if tx.Type() == types.BlobTxType {
return miner.commitBlobTransaction(env, tx)
}
- receipt, err := miner.applyTransaction(env, tx)
+ receipt, bal, err := miner.applyTransaction(env, tx)
if err != nil {
return err
}
@@ -380,6 +373,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx
env.receipts = append(env.receipts, receipt)
env.size += tx.Size()
env.tcount++
+ env.bal.Merge(bal)
return nil
}
@@ -396,7 +390,7 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio
if env.blobs+len(sc.Blobs) > maxBlobs {
return errors.New("max data blobs reached")
}
- receipt, err := miner.applyTransaction(env, tx)
+ receipt, bal, err := miner.applyTransaction(env, tx)
if err != nil {
return err
}
@@ -408,23 +402,24 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio
env.size += txNoBlob.Size()
*env.header.BlobGasUsed += receipt.BlobGasUsed
env.tcount++
+ env.bal.Merge(bal)
return nil
}
// applyTransaction runs the transaction. If execution fails, state and gas pool are reverted.
-func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) {
+func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) {
var (
snap = env.state.Snapshot()
gp = env.gasPool.Snapshot()
)
- receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx)
+ receipt, bal, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx)
if err != nil {
env.state.RevertToSnapshot(snap)
env.gasPool.Set(gp)
- return nil, err
+ return nil, nil, err
}
env.header.GasUsed = env.gasPool.Used()
- return receipt, nil
+ return receipt, bal, nil
}
func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error {
@@ -518,7 +513,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl
continue
}
// Start executing the transaction
- env.state.SetTxContext(tx.Hash(), env.tcount)
+ env.state.SetTxContext(tx.Hash(), env.tcount, uint32(env.tcount+1))
err := miner.commitTransaction(ctx, env, tx)
switch {
@@ -617,10 +612,14 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3
// totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order.
func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int {
+ baseFee := block.BaseFee()
feesWei := new(big.Int)
+ var gasUsed, product big.Int
for i, tx := range block.Transactions() {
- minerFee, _ := tx.EffectiveGasTip(block.BaseFee())
- feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee))
+ minerFee, _ := tx.EffectiveGasTip(baseFee)
+ gasUsed.SetUint64(receipts[i].GasUsed)
+ product.Mul(&gasUsed, minerFee)
+ feesWei.Add(feesWei, &product)
}
return feesWei
}
diff --git a/node/defaults.go b/node/defaults.go
index 403a7f88a3..3410fa2ae5 100644
--- a/node/defaults.go
+++ b/node/defaults.go
@@ -76,6 +76,9 @@ var DefaultConfig = Config{
DiscoveryV5: true,
},
DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported
+ OpenTelemetry: OpenTelemetryConfig{
+ SampleRatio: 1.0,
+ },
}
// DefaultDataDir is the default data directory to use for the databases and other
diff --git a/node/node.go b/node/node.go
index 01318881d4..56ecd7d522 100644
--- a/node/node.go
+++ b/node/node.go
@@ -35,7 +35,6 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
- "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
@@ -44,7 +43,6 @@ import (
// Node is a container on which services can be registered.
type Node struct {
- eventmux *event.TypeMux
config *Config
accman *accounts.Manager
log log.Logger
@@ -108,7 +106,6 @@ func New(conf *Config) (*Node, error) {
node := &Node{
config: conf,
inprocHandler: server,
- eventmux: new(event.TypeMux),
log: conf.Logger,
stop: make(chan struct{}),
server: &p2p.Server{Config: conf.P2P},
@@ -692,12 +689,6 @@ func (n *Node) WSAuthEndpoint() string {
return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix
}
-// EventMux retrieves the event multiplexer used by all the network services in
-// the current protocol stack.
-func (n *Node) EventMux() *event.TypeMux {
- return n.eventmux
-}
-
// OpenDatabaseWithOptions opens an existing database with the given name (or creates one if no
// previous can be found) from within the node's instance directory. If the node has no
// data directory, an in-memory database is returned.
diff --git a/node/rpcstack.go b/node/rpcstack.go
index 20d488b734..1db2ed3f44 100644
--- a/node/rpcstack.go
+++ b/node/rpcstack.go
@@ -463,6 +463,7 @@ func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Either invalid (too many colons) or no port specified
host = r.Host
}
+ host = strings.ToLower(host)
if ipAddr := net.ParseIP(host); ipAddr != nil {
// It's an IP address, we can serve that
h.next.ServeHTTP(w, r)
diff --git a/node/rpcstack_test.go b/node/rpcstack_test.go
index bd75dac4eb..f5668abb08 100644
--- a/node/rpcstack_test.go
+++ b/node/rpcstack_test.go
@@ -60,6 +60,9 @@ func TestVhosts(t *testing.T) {
resp := rpcRequest(t, url, testMethod, "host", "test")
assert.Equal(t, resp.StatusCode, http.StatusOK)
+ respUpper := rpcRequest(t, url, testMethod, "host", "TeSt:1234")
+ assert.Equal(t, respUpper.StatusCode, http.StatusOK)
+
resp2 := rpcRequest(t, url, testMethod, "host", "bad")
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
}
diff --git a/p2p/dial.go b/p2p/dial.go
index f9463d6d89..0ffcd10497 100644
--- a/p2p/dial.go
+++ b/p2p/dial.go
@@ -67,7 +67,10 @@ type tcpDialer struct {
}
func (t tcpDialer) Dial(ctx context.Context, dest *enode.Node) (net.Conn, error) {
- addr, _ := dest.TCPEndpoint()
+ addr, ok := dest.TCPEndpoint()
+ if !ok {
+ return nil, errNoPort
+ }
return t.d.DialContext(ctx, "tcp", addr.String())
}
diff --git a/p2p/discover/table.go b/p2p/discover/table.go
index 721cd7b589..016a2d1af3 100644
--- a/p2p/discover/table.go
+++ b/p2p/discover/table.go
@@ -753,6 +753,41 @@ func (tab *Table) deleteNode(n *enode.Node) {
// waitForNodes blocks until the table contains at least n nodes.
func (tab *Table) waitForNodes(ctx context.Context, n int) error {
+ // Wrap ctx so the forwarder goroutine exits when waitForNodes returns,
+ // regardless of whether the caller's ctx is canceled.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // Set up a notification channel that gets unblocked when there was any activity on
+ // the table. Ultimately this reads from the table's nodeFeed, but can't use the feed
+ // directly on the same goroutine that takes Table.mutex, it would deadlock.
+ var notify chan struct{}
+ var notifyErr error
+ initsub := func() event.Subscription {
+ notify = make(chan struct{}, 1)
+ newnode := make(chan *enode.Node, 1)
+ sub := tab.nodeFeed.Subscribe(newnode)
+ go func() {
+ defer close(notify)
+ for {
+ select {
+ case <-newnode:
+ select {
+ case notify <- struct{}{}:
+ default:
+ }
+ case <-ctx.Done():
+ notifyErr = ctx.Err()
+ return
+ case <-tab.closeReq:
+ notifyErr = errClosed
+ return
+ }
+ }
+ }()
+ return sub
+ }
+
getlength := func() (count int) {
for _, b := range &tab.buckets {
count += len(b.entries)
@@ -760,28 +795,24 @@ func (tab *Table) waitForNodes(ctx context.Context, n int) error {
return count
}
- var ch chan *enode.Node
for {
tab.mutex.Lock()
if getlength() >= n {
tab.mutex.Unlock()
return nil
}
- if ch == nil {
- // Init subscription.
- ch = make(chan *enode.Node)
- sub := tab.nodeFeed.Subscribe(ch)
+ if notify == nil {
+ // Lazily init the subscription. Do this while holding the
+ // lock so we don't miss any events that change the node count.
+ sub := initsub()
defer sub.Unsubscribe()
}
tab.mutex.Unlock()
- // Wait for a node add event.
- select {
- case <-ch:
- case <-ctx.Done():
- return ctx.Err()
- case <-tab.closeReq:
- return errClosed
+ // Wait for table event.
+ if _, ok := <-notify; !ok {
+ break
}
}
+ return notifyErr
}
diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go
index c3b71ea5a6..a16b4d9cab 100644
--- a/p2p/discover/table_test.go
+++ b/p2p/discover/table_test.go
@@ -17,6 +17,7 @@
package discover
import (
+ "context"
"crypto/ecdsa"
"fmt"
"math/rand"
@@ -550,6 +551,45 @@ func TestSetFallbackNodes_DNSHostname(t *testing.T) {
t.Logf("resolved localhost to %v", resolved.IPAddr())
}
+// This test checks that waitForNodes does not block addFoundNode.
+// See https://github.com/ethereum/go-ethereum/issues/34881.
+func TestTable_waitForNodesLocking(t *testing.T) {
+ transport := newPingRecorder()
+ tab, db := newTestTable(transport, Config{})
+ defer db.Close()
+ defer tab.close()
+ <-tab.initDone
+
+ // waitForNodes will never reach this count, so it stays subscribed
+ // to nodeFeed and looping for the duration of the test.
+ waitCtx, cancelWait := context.WithCancel(context.Background())
+ defer cancelWait()
+ waitDone := make(chan struct{})
+ go func() {
+ defer close(waitDone)
+ tab.waitForNodes(waitCtx, 1<<20)
+ }()
+
+ // Call addFoundNode in loop to send to the feed.
+ addDone := make(chan struct{})
+ go func() {
+ defer close(addDone)
+ for i := range 10000 {
+ d := 240 + (i % 17)
+ n := nodeAtDistance(tab.self().ID(), d, intIP(i))
+ tab.addFoundNode(n, true)
+ }
+ }()
+
+ select {
+ case <-addDone:
+ cancelWait()
+ <-waitDone
+ case <-time.After(10 * time.Second):
+ t.Fatal("deadlock detected: add loop did not finish within 10s")
+ }
+}
+
func newkey() *ecdsa.PrivateKey {
key, err := crypto.GenerateKey()
if err != nil {
diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go
index 9e824dae18..ae8cbec3e2 100644
--- a/p2p/discover/v4_udp.go
+++ b/p2p/discover/v4_udp.go
@@ -447,6 +447,7 @@ func (t *UDPv4) loop() {
// Start the timer so it fires when the next pending reply has expired.
now := time.Now()
for p, el := range iterList[*replyMatcher](plist) {
+ nextTimeout = p
if dist := p.deadline.Sub(now); dist < 2*respTimeout {
timeout.Reset(dist)
return
@@ -454,7 +455,7 @@ func (t *UDPv4) loop() {
// Remove pending replies whose deadline is too far in the
// future. These can occur if the system clock jumped
// backwards after the deadline was assigned.
- nextTimeout.errc <- errClockWarp
+ p.errc <- errClockWarp
plist.Remove(el)
}
nextTimeout = nil
@@ -554,8 +555,9 @@ func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) {
if err := t.handlePacket(from, buf[:nbytes]); err != nil && unhandled == nil {
t.log.Debug("Bad discv4 packet", "addr", from, "err", err)
} else if err != nil && unhandled != nil {
+ p := ReadPacket{bytes.Clone(buf[:nbytes]), from}
select {
- case unhandled <- ReadPacket{buf[:nbytes], from}:
+ case unhandled <- p:
default:
}
}
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 9da275c486..3e36b83547 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -186,6 +186,16 @@ const (
HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935.
MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block
+
+ // BALItemCost is the gas-cost divisor for the EIP-7928 block access list
+ // size constraint: bal_items <= block_gas_limit / BALItemCost, where
+ // bal_items counts every distinct address in the BAL plus every storage
+ // key (writes + reads) carried by those accounts.
+ //
+ // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so
+ // the bound has a small safety margin for system-contract accesses that
+ // don't consume block gas.
+ BALItemCost uint64 = 2000
)
// Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation
diff --git a/signer/core/api.go b/signer/core/api.go
index 12acf925f0..3b7b53a312 100644
--- a/signer/core/api.go
+++ b/signer/core/api.go
@@ -196,8 +196,9 @@ func MetadataFromContext(ctx context.Context) Metadata {
if info.Transport != "" {
if info.Transport == "http" {
m.Scheme = info.HTTP.Version
+ } else {
+ m.Scheme = info.Transport
}
- m.Scheme = info.Transport
}
if info.RemoteAddr != "" {
m.Remote = info.RemoteAddr
diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go
index c62b513145..d8b6ef0674 100644
--- a/signer/core/signed_data.go
+++ b/signer/core/signed_data.go
@@ -17,6 +17,7 @@
package core
import (
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -309,7 +310,8 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hex
if sig[64] != 27 && sig[64] != 28 {
return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)")
}
- sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
+ sig = bytes.Clone(sig) // Avoid mutating the input
+ sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
hash := accounts.TextHash(data)
rpk, err := crypto.SigToPub(hash, sig)
if err != nil {
diff --git a/tests/state_test.go b/tests/state_test.go
index 8444d211cf..cf1d4bce4c 100644
--- a/tests/state_test.go
+++ b/tests/state_test.go
@@ -35,7 +35,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
- "github.com/holiman/uint256"
)
func initMatcher(st *testMatcher) {
@@ -329,7 +328,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
initialGas := vm.NewGasBudget(msg.GasLimit)
// Execute the message.
- _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), uint256.MustFromBig(msg.Value))
+ _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value)
if err != nil {
b.Error(err)
return
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index f7cf1c0697..e33e15fc8c 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -479,15 +479,15 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess
From: from,
To: to,
Nonce: tx.Nonce,
- Value: value,
+ Value: uint256.MustFromBig(value),
GasLimit: gasLimit,
- GasPrice: gasPrice,
- GasFeeCap: tx.MaxFeePerGas,
- GasTipCap: tx.MaxPriorityFeePerGas,
+ GasPrice: uint256.MustFromBig(gasPrice),
+ GasFeeCap: uint256.MustFromBig(tx.MaxFeePerGas),
+ GasTipCap: uint256.MustFromBig(tx.MaxPriorityFeePerGas),
Data: data,
AccessList: accessList,
BlobHashes: tx.BlobVersionedHashes,
- BlobGasFeeCap: tx.BlobGasFeeCap,
+ BlobGasFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap),
SetCodeAuthorizations: authList,
}
return msg, nil
diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go
index 8b8d0357bf..572c109f1e 100644
--- a/tests/transaction_test_util.go
+++ b/tests/transaction_test_util.go
@@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error {
return
}
// Intrinsic gas
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
if err != nil {
return
}
@@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error {
if rules.IsPrague {
var floorDataGas uint64
- floorDataGas, err = core.FloorDataGas(rules, tx.Data())
+ floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
return
}
diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go
index e7f57d45a2..3516bf6bd5 100644
--- a/trie/bintrie/binary_node.go
+++ b/trie/bintrie/binary_node.go
@@ -27,8 +27,19 @@ const (
NodeTypeBytes = 1 // Size of node type prefix in serialization
HashSize = 32 // Size of a hash in bytes
StemBitmapSize = 32 // Size of the bitmap in a stem node (256 values = 32 bytes)
+
+ MaxGroupDepth = 8
)
+// bitmapSizeForDepth returns the bitmap size in bytes for a given group depth.
+// For depths 1-3, returns 1 byte. For depths 4-8, returns 2^(depth-3) bytes.
+func bitmapSizeForDepth(groupDepth int) int {
+ if groupDepth <= 3 {
+ return 1
+ }
+ return 1 << (groupDepth - 3)
+}
+
const (
nodeTypeStem = iota + 1
nodeTypeInternal
diff --git a/trie/bintrie/binary_node_test.go b/trie/bintrie/binary_node_test.go
index 12ac199903..857060a0c0 100644
--- a/trie/bintrie/binary_node_test.go
+++ b/trie/bintrie/binary_node_test.go
@@ -23,8 +23,8 @@ import (
"github.com/ethereum/go-ethereum/common"
)
-// TestSerializeDeserializeInternalNode tests flat 65-byte serialization and
-// deserialization of InternalNode through nodeStore.
+// TestSerializeDeserializeInternalNode tests grouped serialization and
+// deserialization of InternalNode through nodeStore at groupDepth=1.
func TestSerializeDeserializeInternalNode(t *testing.T) {
leftHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
rightHash := common.HexToHash("0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")
@@ -39,24 +39,32 @@ func TestSerializeDeserializeInternalNode(t *testing.T) {
rootNode.right = rightRef
s.root = rootRef
- // Serialize the node — flat 65-byte format
- serialized := s.serializeNode(rootRef)
+ // Serialize the node — grouped format at groupDepth=1:
+ // [type(1)][groupDepth(1)][bitmap(1)][leftHash(32)][rightHash(32)] = 67 bytes
+ serialized := s.serializeNode(rootRef, 1)
- // Check the serialized format: [type(1)][leftHash(32)][rightHash(32)]
if serialized[0] != nodeTypeInternal {
t.Errorf("Expected type byte to be %d, got %d", nodeTypeInternal, serialized[0])
}
+ if serialized[1] != 1 {
+ t.Errorf("Expected groupDepth byte to be 1, got %d", serialized[1])
+ }
- expectedLen := NodeTypeBytes + 2*HashSize // 1 + 64 = 65
+ expectedLen := NodeTypeBytes + 1 + 1 + 2*HashSize // type + groupDepth + bitmap + 2 hashes = 67
if len(serialized) != expectedLen {
t.Errorf("Expected serialized length to be %d, got %d", expectedLen, len(serialized))
}
- // Check that left and right hashes are embedded directly
- if !bytes.Equal(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], leftHash[:]) {
+ // Both children present at a 1-level group → bitmap byte = 0b11000000.
+ if serialized[2] != 0xc0 {
+ t.Errorf("Expected bitmap byte 0xc0, got 0x%02x", serialized[2])
+ }
+
+ hashesStart := NodeTypeBytes + 1 + 1
+ if !bytes.Equal(serialized[hashesStart:hashesStart+HashSize], leftHash[:]) {
t.Error("Left hash not found at expected position")
}
- if !bytes.Equal(serialized[NodeTypeBytes+HashSize:], rightHash[:]) {
+ if !bytes.Equal(serialized[hashesStart+HashSize:], rightHash[:]) {
t.Error("Right hash not found at expected position")
}
@@ -116,7 +124,7 @@ func TestSerializeDeserializeStemNode(t *testing.T) {
}
// Serialize the node
- serialized := s.serializeNode(ref)
+ serialized := s.serializeNode(ref, 8)
// Check the serialized format
if serialized[0] != nodeTypeStem {
@@ -195,8 +203,9 @@ func TestDeserializeInvalidType(t *testing.T) {
// TestDeserializeInvalidLength tests deserialization with invalid data length.
func TestDeserializeInvalidLength(t *testing.T) {
s := newNodeStore()
- // InternalNode with valid type byte but wrong length (needs exactly 65 bytes)
- invalidData := []byte{nodeTypeInternal, 0, 0, 0}
+ // InternalNode group header with groupDepth=1 (valid) and a 1-byte bitmap
+ // announcing two present hashes, but the hash payload is missing.
+ invalidData := []byte{nodeTypeInternal, 1, 0xc0}
_, err := s.deserializeNode(invalidData, 0)
if err == nil {
@@ -208,6 +217,21 @@ func TestDeserializeInvalidLength(t *testing.T) {
}
}
+// TestDeserializeInvalidGroupDepth tests deserialization when the group depth
+// byte is out of the supported 1..MaxGroupDepth range.
+func TestDeserializeInvalidGroupDepth(t *testing.T) {
+ s := newNodeStore()
+ invalidData := []byte{nodeTypeInternal, 0, 0, 0}
+
+ _, err := s.deserializeNode(invalidData, 0)
+ if err == nil {
+ t.Fatal("Expected error for invalid group depth, got nil")
+ }
+ if err.Error() != "invalid group depth" {
+ t.Errorf("Expected 'invalid group depth' error, got: %v", err)
+ }
+}
+
// TestKeyToPath tests the keyToPath function.
func TestKeyToPath(t *testing.T) {
tests := []struct {
diff --git a/trie/bintrie/hashed_node_test.go b/trie/bintrie/hashed_node_test.go
index ae77b7c570..2e12bfba5e 100644
--- a/trie/bintrie/hashed_node_test.go
+++ b/trie/bintrie/hashed_node_test.go
@@ -95,7 +95,7 @@ func TestHashedNodeInsertValuesAtStem(t *testing.T) {
sn.setValue(byte(i), v)
}
}
- serialized := rs.serializeNode(ref)
+ serialized := rs.serializeNode(ref, 8)
validResolver := func(path []byte, hash common.Hash) ([]byte, error) {
return serialized, nil
diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go
index 8d5a75de8c..4d8da8af37 100644
--- a/trie/bintrie/internal_node_test.go
+++ b/trie/bintrie/internal_node_test.go
@@ -90,7 +90,7 @@ func TestInternalNodeGetWithResolver(t *testing.T) {
ref := rs.newStemRef(stem, 1)
sn := rs.getStem(ref.Index())
sn.setValue(5, common.HexToHash("0xabcd").Bytes())
- return rs.serializeNode(ref), nil
+ return rs.serializeNode(ref, 8), nil
}
return nil, errors.New("node not found")
}
@@ -290,10 +290,7 @@ func TestInternalNodeCollectNodes(t *testing.T) {
collectedPaths = append(collectedPaths, pathCopy)
}
- err := s.collectNodes(s.root, []byte{1}, flushFn)
- if err != nil {
- t.Fatalf("Failed to collect nodes: %v", err)
- }
+ s.collectNodes(s.root, []byte{1}, flushFn, 8)
// Should have collected 3 nodes: left stem, right stem, and the internal node itself
if len(collectedPaths) != 3 {
diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go
index 31645430c3..a920f91378 100644
--- a/trie/bintrie/iterator.go
+++ b/trie/bintrie/iterator.go
@@ -205,7 +205,7 @@ func (it *binaryNodeIterator) Path() []byte {
}
func (it *binaryNodeIterator) NodeBlob() []byte {
- return it.store.serializeNode(it.current)
+ return it.store.serializeNode(it.current, it.trie.groupDepth)
}
// Leaf reports whether the iterator is currently positioned at a leaf value.
diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go
index 5faf903fba..ae6b57ab34 100644
--- a/trie/bintrie/stem_node_test.go
+++ b/trie/bintrie/stem_node_test.go
@@ -320,10 +320,7 @@ func TestStemNodeCollectNodes(t *testing.T) {
collectedPaths = append(collectedPaths, pathCopy)
}
- err := s.collectNodes(s.root, []byte{0, 1, 0}, flushFn)
- if err != nil {
- t.Fatalf("Failed to collect nodes: %v", err)
- }
+ s.collectNodes(s.root, []byte{0, 1, 0}, flushFn, 8)
// Should have collected one node (itself)
if len(collectedPaths) != 1 {
diff --git a/trie/bintrie/store_commit.go b/trie/bintrie/store_commit.go
index 7101087b51..b14bffbc6c 100644
--- a/trie/bintrie/store_commit.go
+++ b/trie/bintrie/store_commit.go
@@ -107,18 +107,83 @@ func (s *nodeStore) hashInternal(idx uint32) common.Hash {
return node.hash
}
-// SerializeNode serializes a node into the flat on-disk format.
-func (s *nodeStore) serializeNode(ref nodeRef) []byte {
+// serializeSubtree recursively collects child hashes from a subtree of InternalNodes.
+// It traverses up to `remainingDepth` levels, storing hashes of bottom-layer children.
+// position tracks the current index (0 to 2^groupDepth - 1) for bitmap placement.
+// hashes collects the hashes of present children, bitmap tracks which positions are present.
+func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position int, absoluteDepth int, bitmap []byte, hashes *[]common.Hash) {
+ if remainingDepth == 0 {
+ // Bottom layer: store hash if not empty
+ switch ref.Kind() {
+ case kindEmpty:
+ // Leave bitmap bit unset, don't add hash
+ return
+ default:
+ // StemNode, HashedNode, or InternalNode at boundary: store hash
+ bitmap[position/8] |= 1 << (7 - (position % 8))
+ *hashes = append(*hashes, s.computeHash(ref))
+ }
+ return
+ }
+
switch ref.Kind() {
case kindInternal:
+ leftPos := position * 2
+ rightPos := position*2 + 1
+ s.serializeSubtree(s.getInternal(ref.Index()).left, remainingDepth-1, leftPos, absoluteDepth+1, bitmap, hashes)
+ s.serializeSubtree(s.getInternal(ref.Index()).right, remainingDepth-1, rightPos, absoluteDepth+1, bitmap, hashes)
+ case kindEmpty:
+ return
+ default:
+ // StemNode or HashedNode encountered before reaching the group's bottom
+ // layer. Compute the leaf bitmap position where this node's hash will
+ // be stored.
+ leafPos := position
+ switch ref.Kind() {
+ case kindStem:
+ sn := s.getStem(ref.Index())
+ // Extend position using the stem's key bits so that
+ // GetValuesAtStem traversal (which follows key bits) finds the hash.
+ for d := 0; d < remainingDepth; d++ {
+ bit := sn.Stem[(absoluteDepth+d)/8] >> (7 - ((absoluteDepth + d) % 8)) & 1
+ leafPos = leafPos*2 + int(bit)
+ }
+ default:
+ // HashedNode or unknown: extend all-left (no key bits available).
+ // This matches the all-zero path that resolveNode would follow.
+ leafPos = position << remainingDepth
+ }
+ bitmap[leafPos/8] |= 1 << (7 - (leafPos % 8))
+ *hashes = append(*hashes, s.computeHash(ref))
+ }
+}
+
+// SerializeNode serializes a node into the flat on-disk format.
+func (s *nodeStore) serializeNode(ref nodeRef, groupDepth int) []byte {
+ switch ref.Kind() {
+ case kindInternal:
+ // InternalNode group: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes
+ bitmapSize := bitmapSizeForDepth(groupDepth)
+ bitmap := make([]byte, bitmapSize)
+ var hashes []common.Hash
+
node := s.getInternal(ref.Index())
- var serialized [NodeTypeBytes + HashSize + HashSize]byte
+ s.serializeSubtree(ref, groupDepth, 0, int(node.depth), bitmap, &hashes)
+
+ // Build serialized output
+ serializedLen := NodeTypeBytes + 1 + bitmapSize + len(hashes)*HashSize
+ serialized := make([]byte, serializedLen)
serialized[0] = nodeTypeInternal
- lh := s.computeHash(node.left)
- rh := s.computeHash(node.right)
- copy(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], lh[:])
- copy(serialized[NodeTypeBytes+HashSize:], rh[:])
- return serialized[:]
+ serialized[1] = byte(groupDepth) // group depth => bitmap size for a sparse group
+ copy(serialized[2:2+bitmapSize], bitmap)
+
+ offset := NodeTypeBytes + 1 + bitmapSize
+ for _, h := range hashes {
+ copy(serialized[offset:offset+HashSize], h.Bytes())
+ offset += HashSize
+ }
+
+ return serialized
case kindStem:
sn := s.getStem(ref.Index())
@@ -163,6 +228,59 @@ func (s *nodeStore) deserializeNodeWithHash(serialized []byte, depth int, hn com
return s.decodeNode(serialized, depth, hn, false, false)
}
+// deserializeSubtree reconstructs an InternalNode subtree from grouped serialization.
+// remainingDepth is how many more levels to build, position is current index in the bitmap,
+// nodeDepth is the actual trie depth for the node being created.
+// hashIdx tracks the current position in the hash data (incremented as hashes are consumed).
+func (s *nodeStore) deserializeSubtree(hn common.Hash, remainingDepth int, position int, nodeDepth int, bitmap []byte, hashData []byte, hashIdx *int, mustRecompute bool, dirty bool) (nodeRef, error) {
+ if remainingDepth == 0 {
+ // Bottom layer: check bitmap and return HashedNode or Empty
+ if bitmap[position/8]>>(7-(position%8))&1 == 1 {
+ if len(hashData) < (*hashIdx+1)*HashSize {
+ return emptyRef, errInvalidSerializedLength
+ }
+ hash := common.BytesToHash(hashData[*hashIdx*HashSize : (*hashIdx+1)*HashSize])
+ *hashIdx++
+ return s.newHashedRef(hash), nil
+ }
+ return emptyRef, nil
+ }
+
+ // Check if this entire subtree is empty by examining all relevant bitmap bits
+ leftPos := position * 2
+ rightPos := position*2 + 1
+
+ // note that the parent might not need root computations, but the children
+ // do, because their hash isn't saved. Hence `mustRecompute` is set to `true`.
+ left, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, leftPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty)
+ if err != nil {
+ return emptyRef, err
+ }
+ right, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, rightPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty)
+ if err != nil {
+ return emptyRef, err
+ }
+
+ // If both children are empty, return Empty
+ if left.IsEmpty() && right.IsEmpty() {
+ return emptyRef, nil
+ }
+
+ ref := s.newInternalRef(nodeDepth)
+ node := s.getInternal(ref.Index())
+ node.left = left
+ node.right = right
+ node.mustRecompute = mustRecompute
+ if !mustRecompute {
+ // mustRecompute will only be false for the root of the subtree,
+ // for which we already know the hash.
+ node.hash = hn
+ node.mustRecompute = false
+ }
+ node.dirty = dirty
+ return ref, nil
+}
+
func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mustRecompute, dirty bool) (nodeRef, error) {
if len(serialized) == 0 {
return emptyRef, nil
@@ -170,31 +288,23 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus
switch serialized[0] {
case nodeTypeInternal:
- if len(serialized) != NodeTypeBytes+2*HashSize {
+ // Grouped format: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes
+ if len(serialized) < NodeTypeBytes+1 {
return emptyRef, errInvalidSerializedLength
}
- var leftHash, rightHash common.Hash
- copy(leftHash[:], serialized[NodeTypeBytes:NodeTypeBytes+HashSize])
- copy(rightHash[:], serialized[NodeTypeBytes+HashSize:])
+ groupDepth := int(serialized[1])
+ if groupDepth < 1 || groupDepth > MaxGroupDepth {
+ return 0, errors.New("invalid group depth")
+ }
+ bitmapSize := bitmapSizeForDepth(groupDepth)
+ if len(serialized) < NodeTypeBytes+1+bitmapSize {
+ return 0, errInvalidSerializedLength
+ }
+ bitmap := serialized[2 : 2+bitmapSize]
+ hashData := serialized[2+bitmapSize:]
- var leftRef, rightRef nodeRef
- if leftHash != (common.Hash{}) {
- leftRef = s.newHashedRef(leftHash)
- }
- if rightHash != (common.Hash{}) {
- rightRef = s.newHashedRef(rightHash)
- }
-
- ref := s.newInternalRef(depth)
- node := s.getInternal(ref.Index())
- node.left = leftRef
- node.right = rightRef
- if !mustRecompute {
- node.hash = hn
- node.mustRecompute = false
- }
- node.dirty = dirty
- return ref, nil
+ hashIdx := 0
+ return s.deserializeSubtree(hn, groupDepth, 0, depth, bitmap, hashData, &hashIdx, mustRecompute, dirty)
case nodeTypeStem:
if len(serialized) < NodeTypeBytes+StemSize+StemBitmapSize {
@@ -230,45 +340,112 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus
// CollectNodes flushes every node that needs flushing via flushfn in post-order.
// Invariant: any ancestor of a node that needs flushing is itself marked, so a
// clean root means the whole subtree is clean.
-func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) error {
+func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn, groupDepth int) {
switch ref.Kind() {
- case kindEmpty:
- return nil
case kindInternal:
node := s.getInternal(ref.Index())
if !node.dirty {
- return nil
+ return
}
- // Reuse path buffer across children: flushfn consumers
- // (NodeSet.AddNode, tracer.Get) clone via string(path), so in-place
- // mutation is safe.
- path = append(path, 0)
- if err := s.collectNodes(node.left, path, flushfn); err != nil {
- return err
+ // Only flush at group boundaries (depth % groupDepth == 0)
+ if int(node.depth)%groupDepth == 0 {
+ // We're at a group boundary - first collect any nodes in deeper groups,
+ // then flush this group
+ s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-1)
+ flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth))
+ node.dirty = false
+ return
}
- path[len(path)-1] = 1
- if err := s.collectNodes(node.right, path, flushfn); err != nil {
- return err
- }
- path = path[:len(path)-1]
- flushfn(path, s.computeHash(ref), s.serializeNode(ref))
- node.dirty = false
- return nil
+ // Not at a group boundary - this shouldn't happen if we're called correctly from root
+ // but handle it by continuing to traverse
+ s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-(int(node.depth)%groupDepth)-1)
case kindStem:
sn := s.getStem(ref.Index())
if !sn.dirty {
- return nil
+ return
}
- flushfn(path, s.computeHash(ref), s.serializeNode(ref))
+ flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth))
sn.dirty = false
- return nil
- case kindHashed:
- return nil // Already committed
+ case kindHashed, kindEmpty:
default:
- return fmt.Errorf("CollectNodes: unexpected kind %d", ref.Kind())
+ panic(fmt.Sprintf("CollectNodes: unexpected kind %d", ref.Kind()))
}
}
+// collectChildGroups traverses within a group to find and collect nodes in the next group.
+// remainingLevels is how many more levels below the current node until we reach the group boundary.
+// When remainingLevels=0, the current node's children are at the next group boundary.
+func (s *nodeStore) collectChildGroups(node *InternalNode, path []byte, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error {
+ if remainingLevels == 0 {
+ // Current node is at depth (groupBoundary - 1), its children are at the next group boundary
+ if !node.left.IsEmpty() {
+ s.collectNodes(node.left, appendBit(path, 0), flushfn, groupDepth)
+ }
+ if !node.right.IsEmpty() {
+ s.collectNodes(node.right, appendBit(path, 1), flushfn, groupDepth)
+ }
+ return nil
+ }
+
+ if !node.left.IsEmpty() {
+ switch node.left.Kind() {
+ case kindInternal:
+ n := s.getInternal(node.left.Index())
+ if err := s.collectChildGroups(n, appendBit(path, 0), flushfn, groupDepth, remainingLevels-1); err != nil {
+ return err
+ }
+ default:
+ extPath := s.extendPathToGroupLeaf(appendBit(path, 0), node.left, remainingLevels)
+ s.collectNodes(node.left, extPath, flushfn, groupDepth)
+ }
+ }
+ if !node.right.IsEmpty() {
+ switch node.right.Kind() {
+ case kindInternal:
+ n := s.getInternal(node.right.Index())
+ if err := s.collectChildGroups(n, appendBit(path, 1), flushfn, groupDepth, remainingLevels-1); err != nil {
+ return err
+ }
+ default:
+ extPath := s.extendPathToGroupLeaf(appendBit(path, 1), node.right, remainingLevels)
+ s.collectNodes(node.right, extPath, flushfn, groupDepth)
+ }
+ }
+ return nil
+}
+
+// extendPathToGroupLeaf extends a storage path to the group's leaf boundary,
+// matching the projection done by serializeSubtree. For StemNodes, the path
+// is extended using the stem's key bits (same as serializeSubtree). For other
+// node types, the path is extended with all-zero (left) bits.
+func (s *nodeStore) extendPathToGroupLeaf(path []byte, node nodeRef, remainingLevels int) []byte {
+ if remainingLevels <= 0 {
+ return path
+ }
+ if node.Kind() == kindStem {
+ sn := s.getStem(node.Index())
+ for _ = range remainingLevels {
+ bit := sn.Stem[len(path)/8] >> (7 - (len(path) % 8)) & 1
+ path = appendBit(path, bit)
+ }
+ } else {
+ // HashedNode or other: all-left extension (matches serializeSubtree's
+ // position << remainingDepth behavior).
+ for _ = range remainingLevels {
+ path = appendBit(path, 0)
+ }
+ }
+ return path
+}
+
+// appendBit appends a bit to a path, returning a new slice
+func appendBit(path []byte, bit byte) []byte {
+ var p [256]byte
+ copy(p[:], path)
+ result := p[:len(path)]
+ return append(result, bit)
+}
+
func (s *nodeStore) toDot(ref nodeRef, parent, path string) string {
switch ref.Kind() {
case kindInternal:
diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go
index 8c69e0aa00..e3436e3df1 100644
--- a/trie/bintrie/trie.go
+++ b/trie/bintrie/trie.go
@@ -107,9 +107,14 @@ func ChunkifyCode(code []byte) ChunkedCode {
// BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864.
type BinaryTrie struct {
- store *nodeStore
- reader *trie.Reader
- tracer *trie.PrevalueTracer
+ store *nodeStore
+ reader *trie.Reader
+ tracer *trie.PrevalueTracer
+ groupDepth int // Number of levels per serialized group (1-8, default 8)
+}
+
+func (t *BinaryTrie) GroupDepth() int {
+ return t.groupDepth
}
// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
@@ -119,15 +124,20 @@ func (t *BinaryTrie) ToDot() string {
}
// NewBinaryTrie creates a new binary trie.
-func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) {
+// groupDepth specifies the number of levels per serialized group (1-8).
+func NewBinaryTrie(root common.Hash, db database.NodeDatabase, groupDepth int) (*BinaryTrie, error) {
+ if groupDepth < 1 || groupDepth > MaxGroupDepth {
+ panic("invalid group depth size")
+ }
reader, err := trie.NewReader(root, common.Hash{}, db)
if err != nil {
return nil, err
}
t := &BinaryTrie{
- store: newNodeStore(),
- reader: reader,
- tracer: trie.NewPrevalueTracer(),
+ store: newNodeStore(),
+ reader: reader,
+ tracer: trie.NewPrevalueTracer(),
+ groupDepth: groupDepth,
}
// Parse the root node if it's not empty
if root != types.EmptyBinaryHash && root != types.EmptyRootHash {
@@ -312,12 +322,9 @@ func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
// Pre-size the path buffer: collectNodes reuses it in-place via
// append/truncate; 32 covers typical binary-trie depth without regrowth.
pathBuf := make([]byte, 0, 32)
- err := t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) {
+ t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) {
nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path)))
- })
- if err != nil {
- panic(fmt.Errorf("CollectNodes failed: %v", err))
- }
+ }, t.groupDepth)
return t.Hash(), nodeset
}
@@ -341,9 +348,10 @@ func (t *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
// Copy creates a deep copy of the trie.
func (t *BinaryTrie) Copy() *BinaryTrie {
return &BinaryTrie{
- store: t.store.Copy(),
- reader: t.reader,
- tracer: t.tracer.Copy(),
+ store: t.store.Copy(),
+ reader: t.reader,
+ tracer: t.tracer.Copy(),
+ groupDepth: t.groupDepth,
}
}
diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go
index 73aacb76c4..8b7d9e46d6 100644
--- a/trie/bintrie/trie_test.go
+++ b/trie/bintrie/trie_test.go
@@ -768,8 +768,9 @@ func TestGetStorageNonMembershipInternalRoot(t *testing.T) {
// flushes only the root-to-leaf path.
func TestCommitSkipCleanSubtrees(t *testing.T) {
tr := &BinaryTrie{
- store: newNodeStore(),
- tracer: trie.NewPrevalueTracer(),
+ store: newNodeStore(),
+ tracer: trie.NewPrevalueTracer(),
+ groupDepth: 1,
}
const n = 200
key := func(i int) [HashSize]byte {
diff --git a/triedb/database.go b/triedb/database.go
index 533097c9e3..ef95169df1 100644
--- a/triedb/database.go
+++ b/triedb/database.go
@@ -31,12 +31,15 @@ import (
// Config defines all necessary options for database.
type Config struct {
- Preimages bool // Flag whether the preimage of node key is recorded
- IsUBT bool // Flag whether the db is holding a verkle tree
- HashDB *hashdb.Config // Configs for hash-based scheme
- PathDB *pathdb.Config // Configs for experimental path-based scheme
+ Preimages bool // Flag whether the preimage of node key is recorded
+ IsUBT bool // Flag whether the db is holding a unified binary tree
+ BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8, default 8)
+ HashDB *hashdb.Config // Configs for hash-based scheme
+ PathDB *pathdb.Config // Configs for experimental path-based scheme
}
+const DefaultBinTrieGroupDepth = 5
+
// HashDefaults represents a config for using hash-based scheme with
// default settings.
var HashDefaults = &Config{
@@ -45,12 +48,13 @@ var HashDefaults = &Config{
HashDB: hashdb.Defaults,
}
-// UBTDefaults represents a config for holding verkle trie data
+// UBTDefaults represents a config for holding unified binary trie data
// using path-based scheme with default settings.
var UBTDefaults = &Config{
- Preimages: false,
- IsUBT: true,
- PathDB: pathdb.Defaults,
+ Preimages: false,
+ IsUBT: true,
+ BinTrieGroupDepth: DefaultBinTrieGroupDepth,
+ PathDB: pathdb.Defaults,
}
// backend defines the methods needed to access/update trie nodes in different
@@ -323,6 +327,16 @@ func (db *Database) Enable(root common.Hash) error {
return pdb.Enable(root)
}
+// AdoptSyncedState activates the database after a snap/2 sync and adopts the
+// flat state populated during sync as-is, skipping regeneration.
+func (db *Database) AdoptSyncedState(root common.Hash) error {
+ pdb, ok := db.backend.(*pathdb.Database)
+ if !ok {
+ return errors.New("not supported")
+ }
+ return pdb.AdoptSyncedState(root)
+}
+
// Journal commits an entire diff hierarchy to disk into a single journal entry.
// This is meant to be used during shutdown to persist the snapshot without
// flattening everything down (bad for reorgs). It's only supported by path-based
@@ -393,3 +407,7 @@ func (db *Database) SnapshotCompleted() bool {
}
return pdb.SnapshotCompleted()
}
+
+func (db *Database) BinTrieGroupDepth() int {
+ return db.config.BinTrieGroupDepth
+}
diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go
index 98975d7fa5..e52949c93e 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -365,16 +365,9 @@ func (db *Database) Disable() error {
return nil
}
-// Enable activates database and resets the state tree with the provided persistent
-// state root once the state sync is finished.
-func (db *Database) Enable(root common.Hash) error {
- db.lock.Lock()
- defer db.lock.Unlock()
-
- // Short circuit if the database is in read only mode.
- if db.readOnly {
- return errDatabaseReadOnly
- }
+// resetForReactivation performs the pathdb-side bookkeeping shared by both
+// Enable and AdoptSyncedState.
+func (db *Database) resetForReactivation(root common.Hash) error {
// Ensure the provided state root matches the stored one.
stored, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil))
if err != nil {
@@ -383,27 +376,40 @@ func (db *Database) Enable(root common.Hash) error {
if stored != root {
return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root)
}
- // Drop the stale state journal in persistent database and
- // reset the persistent state id back to zero.
+ // Drop the stale state journal marker and reset the persistent state id
+ // back to zero.
batch := db.diskdb.NewBatch()
rawdb.DeleteSnapshotRoot(batch)
rawdb.WritePersistentStateID(batch, 0)
if err := batch.Write(); err != nil {
return err
}
- // Clean up all state histories in freezer. Theoretically
- // all root->id mappings should be removed as well. Since
- // mappings can be huge and might take a while to clear
- // them, just leave them in disk and wait for overwriting.
+ // Clean up all state histories in the freezer. Theoretically all root->id
+ // mappings should be removed as well; since those can be huge, leave them
+ // on disk and let them be overwritten.
purgeHistory(db.stateFreezer, db.diskdb, typeStateHistory)
purgeHistory(db.trienodeFreezer, db.diskdb, typeTrienodeHistory)
- // Re-enable the database as the final step.
+ // Re-enable the database as the final bookkeeping step.
db.waitSync = false
rawdb.WriteSnapSyncStatusFlag(db.diskdb, rawdb.StateSyncFinished)
+ return nil
+}
- // Re-construct a new disk layer backed by persistent state
- // and schedule the state snapshot generation if it's permitted.
+// Enable activates the database after a snap/1 sync and schedules background
+// regeneration of the snapshot from the trie.
+func (db *Database) Enable(root common.Hash) error {
+ db.lock.Lock()
+ defer db.lock.Unlock()
+
+ if db.readOnly {
+ return errDatabaseReadOnly
+ }
+ if err := db.resetForReactivation(root); err != nil {
+ return err
+ }
+ // Re-construct a new disk layer backed by persistent state and schedule
+ // the state snapshot generation if it's permitted.
db.tree.init(generateSnapshot(db, root, db.isUBT || db.config.SnapshotNoBuild))
// After snap sync, the state of the database may have changed completely.
@@ -416,6 +422,43 @@ func (db *Database) Enable(root common.Hash) error {
return nil
}
+// AdoptSyncedState reactivates the database after a snap/2 sync. The syncer
+// already wrote a consistent flat state, so we take it as-is instead of
+// rebuilding it from the trie. The new disk layer has no generator attached,
+// and a "done" marker is written so future boots know the snapshot is
+// already complete.
+func (db *Database) AdoptSyncedState(root common.Hash) error {
+ db.lock.Lock()
+ defer db.lock.Unlock()
+
+ if db.readOnly {
+ return errDatabaseReadOnly
+ }
+ if err := db.resetForReactivation(root); err != nil {
+ return err
+ }
+
+ // Tell the snapshot subsystem the flat state is good by writing the new root
+ // and a "done" marker (nil journal) so the next boot doesn't try to rebuild it.
+ batch := db.diskdb.NewBatch()
+ rawdb.WriteSnapshotRoot(batch, root)
+ journalProgress(batch, nil, nil)
+ if err := batch.Write(); err != nil {
+ return err
+ }
+
+ // New disk layer, no generator attached. Nothing to rebuild, and reads
+ // can serve the flat state right away without waiting on a generator to
+ // scan past every key.
+ dl := newDiskLayer(root, 0, db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0), nil)
+ db.tree.init(dl)
+
+ db.setHistoryIndexer()
+
+ log.Info("Adopted synced state", "root", root)
+ return nil
+}
+
// Recover rollbacks the database to a specified historical point.
// The state is supported as the rollback destination only if it's
// canonical state and the corresponding trie histories are existent.
diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go
index 8ceb22eaba..41212dc9d0 100644
--- a/triedb/pathdb/database_test.go
+++ b/triedb/pathdb/database_test.go
@@ -748,6 +748,84 @@ func TestDisable(t *testing.T) {
}
}
+// TestAdoptSyncedState verifies that AdoptSyncedState rejects a wrong root,
+// writes the on-disk markers that say the snapshot is already complete,
+// leaves a single fresh disk layer with no generator attached, and clears
+// out stale state histories.
+func TestAdoptSyncedState(t *testing.T) {
+ maxDiffLayers = 4
+ defer func() {
+ maxDiffLayers = 128
+ }()
+
+ tester := newTester(t, &testerConfig{layers: 12})
+ defer tester.release()
+
+ // Push everything down to disk so the trie root is the persistent root.
+ if err := tester.db.Commit(tester.lastHash(), false); err != nil {
+ t.Fatalf("Failed to commit, err: %v", err)
+ }
+ stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
+
+ // Mimic the snap-syncing state.
+ if err := tester.db.Disable(); err != nil {
+ t.Fatalf("Failed to disable database: %v", err)
+ }
+ // Mismatched root must be rejected.
+ if err := tester.db.AdoptSyncedState(types.EmptyRootHash); err == nil {
+ t.Fatal("Mismatched root should be rejected")
+ }
+ if err := tester.db.AdoptSyncedState(stored); err != nil {
+ t.Fatalf("AdoptSyncedState failed: %v", err)
+ }
+
+ // On-disk markers reflect a completed snapshot.
+ if got := rawdb.ReadSnapshotRoot(tester.db.diskdb); got != stored {
+ t.Fatalf("SnapshotRoot mismatch: got %x want %x", got, stored)
+ }
+ if blob := rawdb.ReadSnapshotGenerator(tester.db.diskdb); len(blob) == 0 {
+ t.Fatal("Generator journal not written")
+ } else {
+ var entry journalGenerator
+ if err := rlp.DecodeBytes(blob, &entry); err != nil {
+ t.Fatalf("Failed to decode generator journal: %v", err)
+ }
+ if !entry.Done {
+ t.Fatal("Generator journal should be marked Done")
+ }
+ // RLP turns a nil slice into an empty one on decode, so check length.
+ if len(entry.Marker) != 0 {
+ t.Fatalf("Generator marker should be empty, got %x", entry.Marker)
+ }
+ }
+ if rawdb.ReadSnapSyncStatusFlag(tester.db.diskdb) != rawdb.StateSyncFinished {
+ t.Fatal("Sync-status flag should be StateSyncFinished")
+ }
+ if tester.db.waitSync {
+ t.Fatal("waitSync should be false after adopt")
+ }
+
+ // State histories are purged.
+ if n, err := tester.db.stateFreezer.Ancients(); err != nil || n != 0 {
+ t.Fatalf("State histories not purged: count=%d err=%v", n, err)
+ }
+
+ // Layer tree has a single disk layer with no generator attached.
+ if got := tester.db.tree.len(); got != 1 {
+ t.Fatalf("Expected single layer, got %d", got)
+ }
+ dl := tester.db.tree.bottom()
+ if dl.rootHash() != stored {
+ t.Fatalf("Disk layer root mismatch: got %x want %x", dl.rootHash(), stored)
+ }
+ if dl.generator != nil {
+ t.Fatal("Disk layer should have no generator after adopt")
+ }
+ if dl.genMarker() != nil {
+ t.Fatal("genMarker should be nil after adopt")
+ }
+}
+
func TestCommit(t *testing.T) {
// Redefine the diff layer depth allowance for faster testing.
maxDiffLayers = 4
diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go
index 2197e85272..191c2fadf5 100644
--- a/triedb/pathdb/iterator_test.go
+++ b/triedb/pathdb/iterator_test.go
@@ -489,7 +489,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) {
if i%8 == 0 {
e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
}
- if i > 50 || i < 85 {
+ if i > 50 && i < 85 {
f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
}
if i%64 == 0 {
diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go
index efcc3f2549..657fbbff27 100644
--- a/triedb/pathdb/journal.go
+++ b/triedb/pathdb/journal.go
@@ -161,7 +161,19 @@ func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher) (*journalGenerator,
// loadLayers loads a pre-existing state layer backed by a key-value store.
func (db *Database) loadLayers() layer {
// Retrieve the root node of persistent state.
- root, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil))
+ var (
+ root common.Hash
+ err error
+ )
+ if db.isUBT {
+ root = rawdb.ReadSnapshotRoot(db.diskdb)
+ if root == (common.Hash{}) {
+ root = types.EmptyBinaryHash
+ }
+ } else {
+ blob := rawdb.ReadAccountTrieNode(db.diskdb, nil)
+ root, err = db.hasher(blob)
+ }
if err != nil {
log.Crit("Failed to compute node hash", "err", err)
}
diff --git a/version/version.go b/version/version.go
index ea1f5fc632..5d402f3009 100644
--- a/version/version.go
+++ b/version/version.go
@@ -19,6 +19,6 @@ package version
const (
Major = 1 // Major version component of the current release
Minor = 17 // Minor version component of the current release
- Patch = 3 // Patch version component of the current release
+ Patch = 4 // Patch version component of the current release
Meta = "unstable" // Version metadata to append to the version string
)