diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index 41defedd00..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,24 +155,49 @@ 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
+ go run build/ci.go install -dlgo -os windows -arch amd64 -cc x86_64-w64-mingw32-gcc
+
+ - name: "Create/upload archive (amd64)"
+ run: |
+ go run build/ci.go archive -os windows -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
env:
- GETH_MINGW: 'C:\msys64\mingw64'
+ WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
+ AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
+
+ - name: "Create/upload NSIS installer (amd64)"
+ run: |
+ go run build/ci.go nsis -arch amd64 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
+ 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
+ go run build/ci.go install -dlgo -os windows -arch 386 -cc i686-w64-mingw32-gcc
+
+ - name: "Create/upload archive (386)"
+ run: |
+ go run build/ci.go archive -os windows -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
env:
- GETH_MINGW: 'C:\msys64\mingw32'
+ WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
+ AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
+
+ - name: "Create/upload NSIS installer (386)"
+ run: |
+ go run build/ci.go nsis -arch 386 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
+ rm -f build/bin/*
+ env:
+ WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }}
+ AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }}
docker:
name: Docker Image
diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml
new file mode 100644
index 0000000000..8f58ebee9a
--- /dev/null
+++ b/.github/workflows/freebsd.yml
@@ -0,0 +1,29 @@
+on:
+ push:
+ branches:
+ - freebsd-github-action
+
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: FreeBSD-build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ submodules: false
+
+ - name: Test in FreeBSD
+ id: test
+ uses: vmactions/freebsd-vm@v1
+ with:
+ release: "15.0"
+ usesh: true
+ prepare: |
+ pkg install -y go
+ run: |
+ freebsd-version
+ uname -a
+ go version
+ go run ./build/ci.go test -p 8
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 507057afe5..3e811072ff 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -97,3 +97,44 @@ jobs:
- name: Run tests
run: go run build/ci.go test -p 8
+
+ windows:
+ name: Windows ${{ matrix.arch }}
+ needs: lint
+ runs-on: [self-hosted, windows, x64]
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: amd64
+ mingw: 'C:\msys64\mingw64'
+ test: true
+ - arch: '386'
+ mingw: 'C:\msys64\mingw32'
+ test: false
+ env:
+ GETH_MINGW: ${{ matrix.mingw }}
+ GETH_CC: ${{ matrix.mingw }}\bin\gcc.exe
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25'
+ cache: false
+
+ - name: Build
+ shell: cmd
+ run: |
+ set PATH=%GETH_MINGW%\bin;%PATH%
+ go run build/ci.go install -arch ${{ matrix.arch }} -cc %GETH_CC%
+
+ - name: Run tests
+ if: matrix.test
+ shell: cmd
+ run: |
+ set PATH=%GETH_MINGW%\bin;%PATH%
+ go run build/ci.go test -arch ${{ matrix.arch }} -cc %GETH_CC% -short -p 8
diff --git a/accounts/abi/abigen/source2.go.tpl b/accounts/abi/abigen/source2.go.tpl
index 3d98cbb700..c517caf6f4 100644
--- a/accounts/abi/abigen/source2.go.tpl
+++ b/accounts/abi/abigen/source2.go.tpl
@@ -183,8 +183,11 @@ var (
// Solidity: {{.Original.String}}
func ({{ decapitalise $contract.Type}} *{{$contract.Type}}) Unpack{{.Normalized.Name}}Event(log *types.Log) (*{{$contract.Type}}{{.Normalized.Name}}, error) {
event := "{{.Original.Name}}"
- if len(log.Topics) == 0 || log.Topics[0] != {{ decapitalise $contract.Type}}.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != {{ decapitalise $contract.Type}}.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new({{$contract.Type}}{{.Normalized.Name}})
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
index b548b6cdae..f0bba246ab 100644
--- a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
+++ b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
@@ -360,8 +360,11 @@ func (CrowdsaleFundTransfer) ContractEventName() string {
// Solidity: event FundTransfer(address backer, uint256 amount, bool isContribution)
func (crowdsale *Crowdsale) UnpackFundTransferEvent(log *types.Log) (*CrowdsaleFundTransfer, error) {
event := "FundTransfer"
- if len(log.Topics) == 0 || log.Topics[0] != crowdsale.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != crowdsale.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(CrowdsaleFundTransfer)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/dao.go.txt b/accounts/abi/abigen/testdata/v2/dao.go.txt
index c246771d6d..0e9adba31e 100644
--- a/accounts/abi/abigen/testdata/v2/dao.go.txt
+++ b/accounts/abi/abigen/testdata/v2/dao.go.txt
@@ -606,8 +606,11 @@ func (DAOChangeOfRules) ContractEventName() string {
// Solidity: event ChangeOfRules(uint256 minimumQuorum, uint256 debatingPeriodInMinutes, int256 majorityMargin)
func (dAO *DAO) UnpackChangeOfRulesEvent(log *types.Log) (*DAOChangeOfRules, error) {
event := "ChangeOfRules"
- if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dAO.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DAOChangeOfRules)
if len(log.Data) > 0 {
@@ -648,8 +651,11 @@ func (DAOMembershipChanged) ContractEventName() string {
// Solidity: event MembershipChanged(address member, bool isMember)
func (dAO *DAO) UnpackMembershipChangedEvent(log *types.Log) (*DAOMembershipChanged, error) {
event := "MembershipChanged"
- if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dAO.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DAOMembershipChanged)
if len(log.Data) > 0 {
@@ -692,8 +698,11 @@ func (DAOProposalAdded) ContractEventName() string {
// Solidity: event ProposalAdded(uint256 proposalID, address recipient, uint256 amount, string description)
func (dAO *DAO) UnpackProposalAddedEvent(log *types.Log) (*DAOProposalAdded, error) {
event := "ProposalAdded"
- if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dAO.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DAOProposalAdded)
if len(log.Data) > 0 {
@@ -736,8 +745,11 @@ func (DAOProposalTallied) ContractEventName() string {
// Solidity: event ProposalTallied(uint256 proposalID, int256 result, uint256 quorum, bool active)
func (dAO *DAO) UnpackProposalTalliedEvent(log *types.Log) (*DAOProposalTallied, error) {
event := "ProposalTallied"
- if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dAO.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DAOProposalTallied)
if len(log.Data) > 0 {
@@ -780,8 +792,11 @@ func (DAOVoted) ContractEventName() string {
// Solidity: event Voted(uint256 proposalID, bool position, address voter, string justification)
func (dAO *DAO) UnpackVotedEvent(log *types.Log) (*DAOVoted, error) {
event := "Voted"
- if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dAO.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DAOVoted)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
index 8ad59e63b1..d0600d7c3e 100644
--- a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
+++ b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
@@ -72,8 +72,11 @@ func (EventCheckerDynamic) ContractEventName() string {
// Solidity: event dynamic(string indexed idxStr, bytes indexed idxDat, string str, bytes dat)
func (eventChecker *EventChecker) UnpackDynamicEvent(log *types.Log) (*EventCheckerDynamic, error) {
event := "dynamic"
- if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(EventCheckerDynamic)
if len(log.Data) > 0 {
@@ -112,8 +115,11 @@ func (EventCheckerEmpty) ContractEventName() string {
// Solidity: event empty()
func (eventChecker *EventChecker) UnpackEmptyEvent(log *types.Log) (*EventCheckerEmpty, error) {
event := "empty"
- if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(EventCheckerEmpty)
if len(log.Data) > 0 {
@@ -154,8 +160,11 @@ func (EventCheckerIndexed) ContractEventName() string {
// Solidity: event indexed(address indexed addr, int256 indexed num)
func (eventChecker *EventChecker) UnpackIndexedEvent(log *types.Log) (*EventCheckerIndexed, error) {
event := "indexed"
- if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(EventCheckerIndexed)
if len(log.Data) > 0 {
@@ -196,8 +205,11 @@ func (EventCheckerMixed) ContractEventName() string {
// Solidity: event mixed(address indexed addr, int256 num)
func (eventChecker *EventChecker) UnpackMixedEvent(log *types.Log) (*EventCheckerMixed, error) {
event := "mixed"
- if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(EventCheckerMixed)
if len(log.Data) > 0 {
@@ -238,8 +250,11 @@ func (EventCheckerUnnamed) ContractEventName() string {
// Solidity: event unnamed(uint256 indexed arg0, uint256 indexed arg1)
func (eventChecker *EventChecker) UnpackUnnamedEvent(log *types.Log) (*EventCheckerUnnamed, error) {
event := "unnamed"
- if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(EventCheckerUnnamed)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
index 3fbabee5a5..5e4a9ecaf0 100644
--- a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
+++ b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
@@ -134,8 +134,11 @@ func (NameConflictLog) ContractEventName() string {
// Solidity: event log(int256 msg, int256 _msg)
func (nameConflict *NameConflict) UnpackLogEvent(log *types.Log) (*NameConflictLog, error) {
event := "log"
- if len(log.Topics) == 0 || log.Topics[0] != nameConflict.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != nameConflict.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(NameConflictLog)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
index d962583e48..0af31a1cfb 100644
--- a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
+++ b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
@@ -136,8 +136,11 @@ func (NumericMethodNameE1TestEvent) ContractEventName() string {
// Solidity: event _1TestEvent(address _param)
func (numericMethodName *NumericMethodName) UnpackE1TestEventEvent(log *types.Log) (*NumericMethodNameE1TestEvent, error) {
event := "_1TestEvent"
- if len(log.Topics) == 0 || log.Topics[0] != numericMethodName.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != numericMethodName.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(NumericMethodNameE1TestEvent)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/overload.go.txt b/accounts/abi/abigen/testdata/v2/overload.go.txt
index ddddd10186..563edf7842 100644
--- a/accounts/abi/abigen/testdata/v2/overload.go.txt
+++ b/accounts/abi/abigen/testdata/v2/overload.go.txt
@@ -114,8 +114,11 @@ func (OverloadBar) ContractEventName() string {
// Solidity: event bar(uint256 i)
func (overload *Overload) UnpackBarEvent(log *types.Log) (*OverloadBar, error) {
event := "bar"
- if len(log.Topics) == 0 || log.Topics[0] != overload.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != overload.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(OverloadBar)
if len(log.Data) > 0 {
@@ -156,8 +159,11 @@ func (OverloadBar0) ContractEventName() string {
// Solidity: event bar(uint256 i, uint256 j)
func (overload *Overload) UnpackBar0Event(log *types.Log) (*OverloadBar0, error) {
event := "bar0"
- if len(log.Topics) == 0 || log.Topics[0] != overload.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != overload.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(OverloadBar0)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/token.go.txt b/accounts/abi/abigen/testdata/v2/token.go.txt
index 6ebc96861b..3bd60a6cdd 100644
--- a/accounts/abi/abigen/testdata/v2/token.go.txt
+++ b/accounts/abi/abigen/testdata/v2/token.go.txt
@@ -386,8 +386,11 @@ func (TokenTransfer) ContractEventName() string {
// Solidity: event Transfer(address indexed from, address indexed to, uint256 value)
func (token *Token) UnpackTransferEvent(log *types.Log) (*TokenTransfer, error) {
event := "Transfer"
- if len(log.Topics) == 0 || log.Topics[0] != token.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != token.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(TokenTransfer)
if len(log.Data) > 0 {
diff --git a/accounts/abi/abigen/testdata/v2/tuple.go.txt b/accounts/abi/abigen/testdata/v2/tuple.go.txt
index 4724fdd351..10b634f3db 100644
--- a/accounts/abi/abigen/testdata/v2/tuple.go.txt
+++ b/accounts/abi/abigen/testdata/v2/tuple.go.txt
@@ -193,8 +193,11 @@ func (TupleTupleEvent) ContractEventName() string {
// Solidity: event TupleEvent((uint256,uint256[],(uint256,uint256)[]) a, (uint256,uint256)[2][] b, (uint256,uint256)[][2] c, (uint256,uint256[],(uint256,uint256)[])[] d, uint256[] e)
func (tuple *Tuple) UnpackTupleEventEvent(log *types.Log) (*TupleTupleEvent, error) {
event := "TupleEvent"
- if len(log.Topics) == 0 || log.Topics[0] != tuple.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != tuple.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(TupleTupleEvent)
if len(log.Data) > 0 {
@@ -234,8 +237,11 @@ func (TupleTupleEvent2) ContractEventName() string {
// Solidity: event TupleEvent2((uint8,uint8)[] arg0)
func (tuple *Tuple) UnpackTupleEvent2Event(log *types.Log) (*TupleTupleEvent2, error) {
event := "TupleEvent2"
- if len(log.Topics) == 0 || log.Topics[0] != tuple.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != tuple.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(TupleTupleEvent2)
if len(log.Data) > 0 {
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/bind/v2/base.go b/accounts/abi/bind/v2/base.go
index 4f2013b4a3..862175ee32 100644
--- a/accounts/abi/bind/v2/base.go
+++ b/accounts/abi/bind/v2/base.go
@@ -35,8 +35,8 @@ import (
const basefeeWiggleMultiplier = 2
var (
- errNoEventSignature = errors.New("no event signature")
- errEventSignatureMismatch = errors.New("event signature mismatch")
+ ErrNoEventSignature = errors.New("no event signature")
+ ErrEventSignatureMismatch = errors.New("event signature mismatch")
)
// SignerFn is a signer function callback when a contract requires a method to
@@ -536,10 +536,10 @@ func (c *BoundContract) WatchLogs(opts *WatchOpts, name string, query ...[]any)
func (c *BoundContract) UnpackLog(out any, event string, log types.Log) error {
// Anonymous events are not supported.
if len(log.Topics) == 0 {
- return errNoEventSignature
+ return ErrNoEventSignature
}
if log.Topics[0] != c.abi.Events[event].ID {
- return errEventSignatureMismatch
+ return ErrEventSignatureMismatch
}
if len(log.Data) > 0 {
if err := c.abi.UnpackIntoInterface(out, event, log.Data); err != nil {
@@ -559,10 +559,10 @@ func (c *BoundContract) UnpackLog(out any, event string, log types.Log) error {
func (c *BoundContract) UnpackLogIntoMap(out map[string]any, event string, log types.Log) error {
// Anonymous events are not supported.
if len(log.Topics) == 0 {
- return errNoEventSignature
+ return ErrNoEventSignature
}
if log.Topics[0] != c.abi.Events[event].ID {
- return errEventSignatureMismatch
+ return ErrEventSignatureMismatch
}
if len(log.Data) > 0 {
if err := c.abi.UnpackIntoMap(out, event, log.Data); err != nil {
diff --git a/accounts/abi/bind/v2/internal/contracts/db/bindings.go b/accounts/abi/bind/v2/internal/contracts/db/bindings.go
index 4ac1652ff7..2fc57fba6d 100644
--- a/accounts/abi/bind/v2/internal/contracts/db/bindings.go
+++ b/accounts/abi/bind/v2/internal/contracts/db/bindings.go
@@ -276,8 +276,11 @@ func (DBInsert) ContractEventName() string {
// Solidity: event Insert(uint256 key, uint256 value, uint256 length)
func (dB *DB) UnpackInsertEvent(log *types.Log) (*DBInsert, error) {
event := "Insert"
- if len(log.Topics) == 0 || log.Topics[0] != dB.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dB.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DBInsert)
if len(log.Data) > 0 {
@@ -318,8 +321,11 @@ func (DBKeyedInsert) ContractEventName() string {
// Solidity: event KeyedInsert(uint256 indexed key, uint256 value)
func (dB *DB) UnpackKeyedInsertEvent(log *types.Log) (*DBKeyedInsert, error) {
event := "KeyedInsert"
- if len(log.Topics) == 0 || log.Topics[0] != dB.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != dB.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(DBKeyedInsert)
if len(log.Data) > 0 {
diff --git a/accounts/abi/bind/v2/internal/contracts/events/bindings.go b/accounts/abi/bind/v2/internal/contracts/events/bindings.go
index 40d2c44a44..2eb5751f23 100644
--- a/accounts/abi/bind/v2/internal/contracts/events/bindings.go
+++ b/accounts/abi/bind/v2/internal/contracts/events/bindings.go
@@ -115,8 +115,11 @@ func (CBasic1) ContractEventName() string {
// Solidity: event basic1(uint256 indexed id, uint256 data)
func (c *C) UnpackBasic1Event(log *types.Log) (*CBasic1, error) {
event := "basic1"
- if len(log.Topics) == 0 || log.Topics[0] != c.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != c.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(CBasic1)
if len(log.Data) > 0 {
@@ -157,8 +160,11 @@ func (CBasic2) ContractEventName() string {
// Solidity: event basic2(bool indexed flag, uint256 data)
func (c *C) UnpackBasic2Event(log *types.Log) (*CBasic2, error) {
event := "basic2"
- if len(log.Topics) == 0 || log.Topics[0] != c.abi.Events[event].ID {
- return nil, errors.New("event signature mismatch")
+ if len(log.Topics) == 0 {
+ return nil, bind.ErrNoEventSignature
+ }
+ if log.Topics[0] != c.abi.Events[event].ID {
+ return nil, bind.ErrEventSignatureMismatch
}
out := new(CBasic2)
if len(log.Data) > 0 {
diff --git a/accounts/abi/bind/v2/lib_test.go b/accounts/abi/bind/v2/lib_test.go
index 11360fc7dd..38dfa74dfa 100644
--- a/accounts/abi/bind/v2/lib_test.go
+++ b/accounts/abi/bind/v2/lib_test.go
@@ -379,16 +379,16 @@ func TestEventUnpackEmptyTopics(t *testing.T) {
if err == nil {
t.Fatal("expected error when unpacking event with empty topics, got nil")
}
- if err.Error() != "event signature mismatch" {
- t.Fatalf("expected 'event signature mismatch' error, got: %v", err)
+ if err != bind.ErrNoEventSignature {
+ t.Fatalf("expected 'no event signature' error, got: %v", err)
}
_, err = c.UnpackBasic2Event(log)
if err == nil {
t.Fatal("expected error when unpacking event with empty topics, got nil")
}
- if err.Error() != "event signature mismatch" {
- t.Fatalf("expected 'event signature mismatch' error, got: %v", err)
+ if err != bind.ErrNoEventSignature {
+ t.Fatalf("expected 'no event signature' error, got: %v", err)
}
}
}
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/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go
index c9a8cdfcef..e49b110e7e 100644
--- a/accounts/keystore/account_cache_test.go
+++ b/accounts/keystore/account_cache_test.go
@@ -68,18 +68,27 @@ func waitWatcherStart(ks *KeyStore) bool {
func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error {
var list []accounts.Account
+ haveAccounts := false
+ haveChange := false
for t0 := time.Now(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) {
- list = ks.Accounts()
- if reflect.DeepEqual(list, wantAccounts) {
- // ks should have also received change notifications
+ if !haveAccounts {
+ list = ks.Accounts()
+ haveAccounts = reflect.DeepEqual(list, wantAccounts)
+ }
+ if !haveChange {
select {
case <-ks.changes:
+ haveChange = true
default:
- return errors.New("wasn't notified of new accounts")
}
+ }
+ if haveAccounts && haveChange {
return nil
}
}
+ if haveAccounts {
+ return errors.New("wasn't notified of new accounts")
+ }
return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts)
}
diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go
index 1bef321cd1..e2fcd1871a 100644
--- a/accounts/keystore/watch.go
+++ b/accounts/keystore/watch.go
@@ -14,8 +14,8 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-//go:build (darwin && !ios && cgo) || freebsd || (linux && !arm64) || netbsd || solaris
-// +build darwin,!ios,cgo freebsd linux,!arm64 netbsd solaris
+//go:build (darwin && !ios && cgo) || freebsd || linux || netbsd || solaris
+// +build darwin,!ios,cgo freebsd linux netbsd solaris
package keystore
diff --git a/accounts/keystore/watch_fallback.go b/accounts/keystore/watch_fallback.go
index e3c133b3f6..ee1b989e63 100644
--- a/accounts/keystore/watch_fallback.go
+++ b/accounts/keystore/watch_fallback.go
@@ -14,8 +14,8 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-//go:build (darwin && !cgo) || ios || (linux && arm64) || windows || (!darwin && !freebsd && !linux && !netbsd && !solaris)
-// +build darwin,!cgo ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris
+//go:build (darwin && !cgo) || ios || windows || (!darwin && !freebsd && !linux && !netbsd && !solaris)
+// +build darwin,!cgo ios windows !darwin,!freebsd,!linux,!netbsd,!solaris
// This is the fallback implementation of directory watching.
// It is used on unsupported platforms.
diff --git a/accounts/scwallet/hub.go b/accounts/scwallet/hub.go
index 1b1899dc8e..185815365e 100644
--- a/accounts/scwallet/hub.go
+++ b/accounts/scwallet/hub.go
@@ -113,7 +113,7 @@ func (hub *Hub) readPairings() error {
}
func (hub *Hub) writePairings() error {
- pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755)
+ pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return err
}
@@ -129,11 +129,8 @@ func (hub *Hub) writePairings() error {
return err
}
- if _, err := pairingFile.Write(pairingData); err != nil {
- return err
- }
-
- return nil
+ _, err = pairingFile.Write(pairingData)
+ return err
}
func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
diff --git a/accounts/usbwallet/hub.go b/accounts/usbwallet/hub.go
index 6f8ac0d8d9..cfa844b345 100644
--- a/accounts/usbwallet/hub.go
+++ b/accounts/usbwallet/hub.go
@@ -26,7 +26,7 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
- "github.com/karalabe/hid"
+ "github.com/ethereum/hid"
)
// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
diff --git a/accounts/usbwallet/wallet.go b/accounts/usbwallet/wallet.go
index f1597ca1a7..5da58a2ec2 100644
--- a/accounts/usbwallet/wallet.go
+++ b/accounts/usbwallet/wallet.go
@@ -31,7 +31,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
- "github.com/karalabe/hid"
+ "github.com/ethereum/hid"
)
// Maximum time between wallet health checks to detect USB unplugs.
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 b460368b84..c733b3f350 100644
--- a/beacon/engine/gen_ed.go
+++ b/beacon/engine/gen_ed.go
@@ -34,7 +34,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
- SlotNumber *hexutil.Uint64 `json:"slotNumber"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
}
var enc ExecutableData
enc.ParentHash = e.ParentHash
@@ -83,7 +83,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
- SlotNumber *hexutil.Uint64 `json:"slotNumber"`
+ SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"`
}
var dec ExecutableData
if err := json.Unmarshal(input, &dec); err != nil {
diff --git a/beacon/engine/types.go b/beacon/engine/types.go
index 5c94e67de1..9b0b186df7 100644
--- a/beacon/engine/types.go
+++ b/beacon/engine/types.go
@@ -99,7 +99,7 @@ type ExecutableData struct {
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *uint64 `json:"blobGasUsed"`
ExcessBlobGas *uint64 `json:"excessBlobGas"`
- SlotNumber *uint64 `json:"slotNumber"`
+ SlotNumber *uint64 `json:"slotNumber,omitempty"`
}
// JSON type overrides for executableData.
@@ -276,7 +276,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()...)
}
diff --git a/build/checksums.txt b/build/checksums.txt
index ba80a3e201..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.7
+# version:golang 1.25.10
# https://go.dev/dl/
-178f2832820274b43e177d32f06a3ebb0129e427dd20a5e4c88df2c1763cf10a go1.25.7.src.tar.gz
-81bf2a1f20633f62d55d826d82dde3b0570cf1408a91e15781b266037299285b go1.25.7.aix-ppc64.tar.gz
-bf5050a2152f4053837b886e8d9640c829dbacbc3370f913351eb0904cb706f5 go1.25.7.darwin-amd64.tar.gz
-ff18369ffad05c57d5bed888b660b31385f3c913670a83ef557cdfd98ea9ae1b go1.25.7.darwin-arm64.tar.gz
-c5dccd7f192dd7b305dc209fb316ac1917776d74bd8e4d532ef2772f305bf42a go1.25.7.dragonfly-amd64.tar.gz
-a2de97c8ac74bf64b0ae73fe9d379e61af530e061bc7f8f825044172ffe61a8b go1.25.7.freebsd-386.tar.gz
-055f9e138787dcafa81eb0314c8ff70c6dd0f6dba1e8a6957fef5d5efd1ab8fd go1.25.7.freebsd-amd64.tar.gz
-60e7f7a7c990f0b9539ac8ed668155746997d404643a4eecd47b3dee1b7e710b go1.25.7.freebsd-arm.tar.gz
-631e03d5fd4c526e2f499154d8c6bf4cb081afb2fff171c428722afc9539d53a go1.25.7.freebsd-arm64.tar.gz
-8a264fd685823808140672812e3ad9c43f6ad59444c0dc14cdd3a1351839ddd5 go1.25.7.freebsd-riscv64.tar.gz
-57c672447d906a1bcab98f2b11492d54521a791aacbb4994a25169e59cbe289a go1.25.7.illumos-amd64.tar.gz
-2866517e9ca81e6a2e85a930e9b11bc8a05cfeb2fc6dc6cb2765e7fb3c14b715 go1.25.7.linux-386.tar.gz
-12e6d6a191091ae27dc31f6efc630e3a3b8ba409baf3573d955b196fdf086005 go1.25.7.linux-amd64.tar.gz
-ba611a53534135a81067240eff9508cd7e256c560edd5d8c2fef54f083c07129 go1.25.7.linux-arm64.tar.gz
-1ba07e0eb86b839e72467f4b5c7a5597d07f30bcf5563c951410454f7cda5266 go1.25.7.linux-armv6l.tar.gz
-775753fc5952a334c415f08768df2f0b73a3228a16e8f5f63d545daacb4e3357 go1.25.7.linux-loong64.tar.gz
-1a023bb367c5fbb4c637a2f6dc23ff17c6591ad929ce16ea88c74d857153b307 go1.25.7.linux-mips.tar.gz
-a8e97223d8aa6fdfd45f132a4784d2f536bbac5f3d63a24b63d33b6bfe1549af go1.25.7.linux-mips64.tar.gz
-eb9edb6223330d5e20275667c65dea076b064c08e595fe4eba5d7d6055cfaccf go1.25.7.linux-mips64le.tar.gz
-9c1e693552a5f9bb9e0012d1c5e01456ecefbc59bef53a77305222ce10aba368 go1.25.7.linux-mipsle.tar.gz
-28a788798e7329acbbc0ac2caa5e4368b1e5ede646cc24429c991214cfb45c63 go1.25.7.linux-ppc64.tar.gz
-42124c0edc92464e2b37b2d7fcd3658f0c47ebd6a098732415a522be8cb88e3f go1.25.7.linux-ppc64le.tar.gz
-88d59c6893c8425875d6eef8e3434bc2fa2552e5ad4c058c6cd8cd710a0301c8 go1.25.7.linux-riscv64.tar.gz
-c6b77facf666dc68195ecab05dbf0ebb4e755b2a8b7734c759880557f1c29b0c go1.25.7.linux-s390x.tar.gz
-f14c184d9ade0ee04c7735d4071257b90896ecbde1b32adae84135f055e6399b go1.25.7.netbsd-386.tar.gz
-7e7389e404dca1088c31f0fc07f1dd60891d7182bcd621469c14f7e79eceb3ff go1.25.7.netbsd-amd64.tar.gz
-70388bb3ef2f03dbf1357e9056bd09034a67e018262557354f8cf549766b3f9d go1.25.7.netbsd-arm.tar.gz
-8c1cda9d25bfc9b18d24d5f95fc23949dd3ff99fa408a6cfa40e2cf12b07e362 go1.25.7.netbsd-arm64.tar.gz
-42f0d1bfbe39b8401cccb84dd66b30795b97bfc9620dfdc17c5cd4fcf6495cb0 go1.25.7.openbsd-386.tar.gz
-e514879c0a28bc32123cd52c4c093de912477fe83f36a6d07517d066ef55391a go1.25.7.openbsd-amd64.tar.gz
-8cd22530695a0218232bf7efea8f162df1697a3106942ac4129b8c3de39ce4ef go1.25.7.openbsd-arm.tar.gz
-938720f6ebc0d1c53d7840321d3a31f29fd02496e84a6538f442a9311dc1cc9a go1.25.7.openbsd-arm64.tar.gz
-a4c378b73b98f89a3596c2ef51aabbb28783d9ca29f7e317d8ca07939660ce6f go1.25.7.openbsd-ppc64.tar.gz
-937b58734fbeaa8c7941a0e4285e7e84b7885396e8d11c23f9ab1a8ff10ff20e go1.25.7.openbsd-riscv64.tar.gz
-61a093c8c5244916f25740316386bb9f141545dcf01b06a79d1c78ece488403e go1.25.7.plan9-386.tar.gz
-7fc8f6689c9de8ccb7689d2278035fa83c2d601409101840df6ddfe09ba58699 go1.25.7.plan9-amd64.tar.gz
-9661dff8eaeeb62f1c3aadbc5ff189a2e6744e1ec885e32dbcb438f58a34def5 go1.25.7.plan9-arm.tar.gz
-28ecba0e1d7950c8b29a4a04962dd49c3bf5221f55a44f17d98f369f82859cf4 go1.25.7.solaris-amd64.tar.gz
-baa6b488291801642fa620026169e38bec2da2ac187cd3ae2145721cf826bbc3 go1.25.7.windows-386.zip
-c75e5f4ff62d085cc0017be3ad19d5536f46825fa05db06ec468941f847e3228 go1.25.7.windows-amd64.zip
-807033f85931bc4a589ca8497535dcbeb1f30d506e47fa200f5f04c4a71c3d9f go1.25.7.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/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go
index efe9144069..4dc2507693 100644
--- a/cmd/devp2p/internal/v5test/discv5tests.go
+++ b/cmd/devp2p/internal/v5test/discv5tests.go
@@ -257,34 +257,50 @@ that they are returned by FINDNODE.`)
// Create bystanders.
nodes := make([]*bystander, 5)
- added := make(chan enode.ID, len(nodes))
+ liveCh := make(chan enode.ID, len(nodes))
for i := range nodes {
- nodes[i] = newBystander(t, s, added)
+ nodes[i] = newBystander(t, s, liveCh)
defer nodes[i].close()
}
- // Get them added to the remote table.
+ // Prefill each bystander with the full bystander set so background FINDNODE
+ // lookups see useful routing data instead of empty responses.
+ known := make([]*enode.Node, 0, len(nodes))
+ for _, bn := range nodes {
+ known = append(known, bn.conn.localNode.Node())
+ }
+ for _, bn := range nodes {
+ bn.known = append([]*enode.Node(nil), known...)
+ }
+
+ // Wait until enough bystanders have actually become live, i.e. the remote node
+ // has revalidated them by sending PING and receiving our PONG.
+ requiredLiveNodes := len(nodes)
timeout := 60 * time.Second
timeoutCh := time.After(timeout)
- for count := 0; count < len(nodes); {
+ liveSet := make(map[enode.ID]*enode.Node)
+ for len(liveSet) < requiredLiveNodes {
select {
- case id := <-added:
- t.Logf("bystander node %v added to remote table", id)
- count++
+ case id := <-liveCh:
+ for _, bn := range nodes {
+ if bn.id() == id {
+ liveSet[id] = bn.conn.localNode.Node()
+ break
+ }
+ }
+ t.Logf("bystander node %v became live", id)
case <-timeoutCh:
- t.Errorf("remote added %d bystander nodes in %v, need %d to continue", count, timeout, len(nodes))
- t.Logf("this can happen if the node has a non-empty table from previous runs")
+ t.Errorf("remote revalidated %d bystander nodes in %v, need %d to continue", len(liveSet), timeout, requiredLiveNodes)
return
}
}
- t.Logf("all %d bystander nodes were added", len(nodes))
+ t.Logf("continuing after all %d bystander nodes became live", len(liveSet))
- // Collect our nodes by distance.
+ // Collect live nodes by distance.
var dists []uint
expect := make(map[enode.ID]*enode.Node)
- for _, bn := range nodes {
- n := bn.conn.localNode.Node()
- expect[n.ID()] = n
+ for id, n := range liveSet {
+ expect[id] = n
d := uint(enode.LogDist(n.ID(), s.Dest.ID()))
if !slices.Contains(dists, d) {
dists = append(dists, d)
@@ -295,42 +311,63 @@ that they are returned by FINDNODE.`)
t.Log("requesting nodes")
conn, l1 := s.listen1(t)
defer conn.close()
- foundNodes, err := conn.findnode(l1, dists)
- if err != nil {
- t.Fatal(err)
- }
- t.Logf("remote returned %d nodes for distance list %v", len(foundNodes), dists)
- for _, n := range foundNodes {
- delete(expect, n.ID())
- }
- if len(expect) > 0 {
- t.Errorf("missing %d nodes in FINDNODE result", len(expect))
- t.Logf("this can happen if the test is run multiple times in quick succession")
- t.Logf("and the remote node hasn't removed dead nodes from previous runs yet")
- } else {
- t.Logf("all %d expected nodes were returned", len(nodes))
+
+ const maxAttempts = 5
+ const retryInterval = 2 * time.Second
+
+ for attempt := 1; attempt <= maxAttempts; attempt++ {
+ foundNodes, err := conn.findnode(l1, dists)
+ if err != nil {
+ t.Fatal(err)
+ }
+ missing := make(map[enode.ID]struct{})
+ for id := range expect {
+ missing[id] = struct{}{}
+ }
+ for _, n := range foundNodes {
+ delete(missing, n.ID())
+ }
+ t.Logf("attempt %d: remote returned %d nodes for distance list %v, missing %d", attempt, len(foundNodes), dists, len(missing))
+ if len(missing) == 0 {
+ t.Logf("all %d expected live nodes were returned", len(expect))
+ return
+ }
+ if attempt < maxAttempts {
+ time.Sleep(retryInterval)
+ }
}
+ t.Errorf("missing nodes in FINDNODE result after %d attempts", maxAttempts)
+ t.Logf("this can happen if the node has a non-empty table from previous runs")
}
// A bystander is a node whose only purpose is filling a spot in the remote table.
type bystander struct {
- dest *enode.Node
- conn *conn
- l net.PacketConn
+ dest *enode.Node
+ conn *conn
+ l net.PacketConn
+ known []*enode.Node
- addedCh chan enode.ID
- done sync.WaitGroup
+ liveCh chan enode.ID
+ sent map[v5wire.Nonce]v5wire.Packet
+ done sync.WaitGroup
}
-func newBystander(t *utesting.T, s *Suite, added chan enode.ID) *bystander {
+func newBystander(t *utesting.T, s *Suite, live chan enode.ID) *bystander {
conn, l := s.listen1(t)
conn.setEndpoint(l) // bystander nodes need IP/port to get pinged
bn := &bystander{
- conn: conn,
- l: l,
- dest: s.Dest,
- addedCh: added,
+ conn: conn,
+ l: l,
+ dest: s.Dest,
+ liveCh: live,
+ sent: make(map[v5wire.Nonce]v5wire.Packet),
}
+ // Establish an initial session and let the remote learn this node before
+ // switching to the passive responder loop below.
+ conn.reqresp(l, &v5wire.Ping{
+ ReqID: conn.nextReqID(),
+ ENRSeq: conn.localNode.Seq(),
+ })
bn.done.Add(1)
go bn.loop()
return bn
@@ -351,48 +388,57 @@ func (bn *bystander) close() {
func (bn *bystander) loop() {
defer bn.done.Done()
- var (
- lastPing time.Time
- wasAdded bool
- )
for {
- // Ping the remote node.
- if !wasAdded && time.Since(lastPing) > 10*time.Second {
- bn.conn.reqresp(bn.l, &v5wire.Ping{
- ReqID: bn.conn.nextReqID(),
- ENRSeq: bn.dest.Seq(),
- })
- lastPing = time.Now()
- }
- // Answer packets.
- switch p := bn.conn.read(bn.l).(type) {
- case *v5wire.Ping:
- bn.conn.write(bn.l, &v5wire.Pong{
- ReqID: p.ReqID,
- ENRSeq: bn.conn.localNode.Seq(),
- ToIP: bn.dest.IP(),
- ToPort: uint16(bn.dest.UDP()),
- }, nil)
- wasAdded = true
- bn.notifyAdded()
- case *v5wire.Findnode:
- bn.conn.write(bn.l, &v5wire.Nodes{ReqID: p.ReqID, RespCount: 1}, nil)
- wasAdded = true
- bn.notifyAdded()
- case *v5wire.TalkRequest:
- bn.conn.write(bn.l, &v5wire.TalkResponse{ReqID: p.ReqID}, nil)
- case *readError:
- if !netutil.IsTemporaryError(p.err) {
- bn.conn.logf("shutting down: %v", p.err)
- return
+ p, from := bn.conn.readFrom(bn.l)
+ switch p := p.(type) {
+ case *v5wire.Whoareyou:
+ p.Node = bn.dest
+ if resp, ok := bn.sent[p.Nonce]; ok {
+ nonce := bn.conn.writeTo(bn.l, resp, p, from)
+ delete(bn.sent, p.Nonce)
+ bn.sent[nonce] = resp
+ } else {
+ bn.conn.writeTo(bn.l, &v5wire.Ping{
+ ReqID: bn.conn.nextReqID(),
+ ENRSeq: bn.conn.localNode.Seq(),
+ }, p, from)
}
+ case *v5wire.Ping:
+ resp := &v5wire.Pong{
+ ReqID: append([]byte(nil), p.ReqID...),
+ ENRSeq: bn.conn.localNode.Seq(),
+ ToIP: from.IP,
+ ToPort: uint16(from.Port),
+ }
+ nonce := bn.conn.writeTo(bn.l, resp, nil, from)
+ bn.sent[nonce] = resp
+ bn.notifyLive()
+ case *v5wire.Findnode:
+ resp := &v5wire.Nodes{ReqID: append([]byte(nil), p.ReqID...), RespCount: 1}
+ for _, n := range bn.known {
+ if slices.Contains(p.Distances, uint(enode.LogDist(n.ID(), bn.id()))) {
+ resp.Nodes = append(resp.Nodes, n.Record())
+ }
+ }
+ nonce := bn.conn.writeTo(bn.l, resp, nil, from)
+ bn.sent[nonce] = resp
+ case *v5wire.TalkRequest:
+ resp := &v5wire.TalkResponse{ReqID: append([]byte(nil), p.ReqID...)}
+ nonce := bn.conn.writeTo(bn.l, resp, nil, from)
+ bn.sent[nonce] = resp
+ case *readError:
+ if netutil.IsTemporaryError(p.err) || v5wire.IsInvalidHeader(p.err) {
+ continue
+ }
+ bn.conn.logf("shutting down: %v", p.err)
+ return
}
}
}
-func (bn *bystander) notifyAdded() {
- if bn.addedCh != nil {
- bn.addedCh <- bn.id()
- bn.addedCh = nil
+func (bn *bystander) notifyLive() {
+ if bn.liveCh != nil {
+ bn.liveCh <- bn.id()
+ bn.liveCh = nil
}
}
diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go
index acab1eef79..0532dafb2c 100644
--- a/cmd/devp2p/internal/v5test/framework.go
+++ b/cmd/devp2p/internal/v5test/framework.go
@@ -127,14 +127,16 @@ func (tc *conn) nextReqID() []byte {
// The request is retried if a handshake is requested.
func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet {
reqnonce := tc.write(c, req, nil)
- switch resp := tc.read(c).(type) {
+ resp, from := tc.readFrom(c)
+ switch resp := resp.(type) {
case *v5wire.Whoareyou:
if resp.Nonce != reqnonce {
return readErrorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:])
}
resp.Node = tc.remote
- tc.write(c, req, resp)
- return tc.read(c)
+ tc.writeTo(c, req, resp, from)
+ resp2, _ := tc.readFrom(c)
+ return resp2
default:
return resp
}
@@ -150,21 +152,24 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error)
results []*enode.Node
)
for n := 1; n > 0; {
- switch resp := tc.read(c).(type) {
+ resp, from := tc.readFrom(c)
+ switch resp := resp.(type) {
case *v5wire.Whoareyou:
// Handle handshake.
if resp.Nonce == reqnonce {
resp.Node = tc.remote
- tc.write(c, findnode, resp)
+ tc.writeTo(c, findnode, resp, from)
} else {
return nil, fmt.Errorf("unexpected WHOAREYOU (nonce %x), waiting for NODES", resp.Nonce[:])
}
case *v5wire.Ping:
// Handle ping from remote.
- tc.write(c, &v5wire.Pong{
+ tc.writeTo(c, &v5wire.Pong{
ReqID: resp.ReqID,
ENRSeq: tc.localNode.Seq(),
- }, nil)
+ ToIP: from.IP,
+ ToPort: uint16(from.Port),
+ }, nil, from)
case *v5wire.Nodes:
// Got NODES! Check request ID.
if !bytes.Equal(resp.ReqID, findnode.ReqID) {
@@ -200,11 +205,16 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error)
// write sends a packet on the given connection.
func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) v5wire.Nonce {
+ return tc.writeTo(c, p, challenge, tc.remoteAddr)
+}
+
+// writeTo sends a packet on the given connection to the given UDP address.
+func (tc *conn) writeTo(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou, to *net.UDPAddr) v5wire.Nonce {
packet, nonce, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge)
if err != nil {
panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err))
}
- if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil {
+ if _, err := c.WriteTo(packet, to); err != nil {
tc.logf("Can't send %s: %v", p.Name(), err)
} else {
tc.logf(">> %s", p.Name())
@@ -214,24 +224,30 @@ func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoar
// read waits for an incoming packet on the given connection.
func (tc *conn) read(c net.PacketConn) v5wire.Packet {
+ p, _ := tc.readFrom(c)
+ return p
+}
+
+// readFrom waits for an incoming packet and returns its source address.
+func (tc *conn) readFrom(c net.PacketConn) (v5wire.Packet, *net.UDPAddr) {
buf := make([]byte, 1280)
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil {
- return &readError{err}
+ return &readError{err}, nil
}
- n, _, err := c.ReadFrom(buf)
+ n, from, err := c.ReadFrom(buf)
if err != nil {
- return &readError{err}
+ return &readError{err}, nil
}
- // Always use tc.remoteAddr for session lookup. The actual source address of
- // the packet may differ from tc.remoteAddr when the remote node is reachable
- // via multiple networks (e.g. Docker bridge vs. overlay), but the codec's
- // session cache is keyed by the address used during Encode.
+ udpFrom, _ := from.(*net.UDPAddr)
+ // Use tc.remoteAddr for codec/session lookup because the fixture keys sessions
+ // by the advertised endpoint, but return the actual UDP source so responses can
+ // comply with the spec and go back to the request envelope address.
_, _, p, err := tc.codec.Decode(buf[:n], tc.remoteAddr.String())
if err != nil {
- return &readError{err}
+ return &readError{err}, udpFrom
}
tc.logf("<< %s", p.Name())
- return p
+ return p, udpFrom
}
// logf prints to the test log.
diff --git a/cmd/devp2p/rlpxcmd.go b/cmd/devp2p/rlpxcmd.go
index 1dc8f82460..ec73171e76 100644
--- a/cmd/devp2p/rlpxcmd.go
+++ b/cmd/devp2p/rlpxcmd.go
@@ -17,6 +17,7 @@
package main
import (
+ "bytes"
"errors"
"fmt"
"net"
@@ -30,6 +31,31 @@ import (
"github.com/urfave/cli/v2"
)
+// decodeRLPxDisconnect parses a disconnect message payload. Per the RLPx spec
+// the payload is a list containing a single reason, but some implementations
+// (including older geth) sent the reason as a bare byte. Accept both forms.
+func decodeRLPxDisconnect(data []byte) (p2p.DiscReason, error) {
+ s := rlp.NewStream(bytes.NewReader(data), uint64(len(data)))
+ k, _, err := s.Kind()
+ if err != nil {
+ return 0, err
+ }
+ var reason p2p.DiscReason
+ if k == rlp.List {
+ if _, err := s.List(); err != nil {
+ return 0, err
+ }
+ if err := s.Decode(&reason); err != nil {
+ return 0, err
+ }
+ return reason, nil
+ }
+ if err := s.Decode(&reason); err != nil {
+ return 0, err
+ }
+ return reason, nil
+}
+
var (
rlpxCommand = &cli.Command{
Name: "rlpx",
@@ -103,11 +129,15 @@ func rlpxPing(ctx *cli.Context) error {
}
fmt.Printf("%+v\n", h)
case 1:
- var msg []p2p.DiscReason
- if rlp.DecodeBytes(data, &msg); len(msg) == 0 {
- return errors.New("invalid disconnect message")
+ // The disconnect message is specified as a list containing the reason,
+ // but some implementations (including older geth) send the reason as a
+ // single byte. Handle both forms, and on failure include the raw payload
+ // so the operator can see what was actually sent.
+ reason, decErr := decodeRLPxDisconnect(data)
+ if decErr != nil {
+ return fmt.Errorf("invalid disconnect message: %v (raw=0x%x)", decErr, data)
}
- return fmt.Errorf("received disconnect message: %v", msg[0])
+ return fmt.Errorf("received disconnect message: %v", reason)
default:
return fmt.Errorf("invalid message code %d, expected handshake (code zero) or disconnect (code one)", code)
}
diff --git a/cmd/devp2p/rlpxcmd_test.go b/cmd/devp2p/rlpxcmd_test.go
new file mode 100644
index 0000000000..d7b374c47d
--- /dev/null
+++ b/cmd/devp2p/rlpxcmd_test.go
@@ -0,0 +1,75 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see .
+
+package main
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/p2p"
+)
+
+func TestDecodeRLPxDisconnect(t *testing.T) {
+ tests := []struct {
+ name string
+ payload []byte
+ want p2p.DiscReason
+ wantErr bool
+ }{
+ {
+ name: "list form (spec-compliant)",
+ payload: []byte{0xc1, 0x04}, // [4] = TooManyPeers
+ want: p2p.DiscTooManyPeers,
+ },
+ {
+ name: "list form with reason zero",
+ payload: []byte{0xc1, 0x80}, // [0] = Requested
+ want: p2p.DiscRequested,
+ },
+ {
+ name: "bare byte form (legacy geth)",
+ payload: []byte{0x04}, // 4 = TooManyPeers
+ want: p2p.DiscTooManyPeers,
+ },
+ {
+ name: "bare byte form zero",
+ payload: []byte{0x80}, // 0 = Requested
+ want: p2p.DiscRequested,
+ },
+ {
+ name: "empty payload",
+ payload: []byte{},
+ wantErr: true,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := decodeRLPxDisconnect(tc.payload)
+ if tc.wantErr {
+ if err == nil {
+ t.Fatalf("expected error, got reason=%v", got)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != tc.want {
+ t.Fatalf("got reason %v, want %v", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/cmd/era/main.go b/cmd/era/main.go
index 1c26f44ad4..43279e7001 100644
--- a/cmd/era/main.go
+++ b/cmd/era/main.go
@@ -337,9 +337,6 @@ func checkAccumulator(e era.Era) error {
// accumulation across the entire set and are verified at the end.
for it.Next() {
// 1) next() walks the block index, so we're able to implicitly verify it.
- if it.Error() != nil {
- return fmt.Errorf("error reading block %d: %w", it.Number(), it.Error())
- }
block, receipts, err := it.BlockAndReceipts()
if err != nil {
return fmt.Errorf("error reading block %d: %w", it.Number(), err)
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index efe22d36f5..a2de58ad46 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -17,9 +17,12 @@
package t8ntool
import (
+ "context"
+ "encoding/json"
"fmt"
stdmath "math"
"math/big"
+ "os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -47,6 +50,9 @@ type Prestate struct {
Env stEnv `json:"env"`
Pre types.GenesisAlloc `json:"pre"`
TreeLeaves map[common.Hash]hexutil.Bytes `json:"vkt,omitempty"`
+ // AllocPath, when non-empty, causes Apply to stream the alloc from disk
+ // instead of reading Pre, so the full map never materializes in memory.
+ AllocPath string `json:"-"`
}
//go:generate go run github.com/fjl/gencodec -type ExecutionResult -field-override executionResultMarshaling -out gen_execresult.go
@@ -146,8 +152,19 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
return h
}
var (
- isEIP4762 = chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp)
- statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762)
+ isEIP4762 = chainConfig.IsUBT(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp)
+ statedb *state.StateDB
+ )
+ if pre.AllocPath != "" {
+ var err error
+ statedb, err = MakePreStateStreaming(rawdb.NewMemoryDatabase(), pre.AllocPath, isEIP4762)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ } else {
+ statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762)
+ }
+ var (
signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp)
gaspool = core.NewGasPool(pre.Env.GasLimit)
blockHash = common.Hash{0x13, 0x37}
@@ -253,7 +270,7 @@ 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()
@@ -315,27 +332,14 @@ 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, 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))
}
-
// Commit block
root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time))
if err != nil {
@@ -378,7 +382,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool) *state.StateDB {
- tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsVerkle: isBintrie})
+ tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie})
sdb := state.NewDatabase(tdb, nil)
root := types.EmptyRootHash
@@ -414,6 +418,76 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool
return statedb
}
+// MakePreStateStreaming is like MakePreState, but decodes the alloc from disk
+// one account at a time so the full map is never held in memory.
+func MakePreStateStreaming(db ethdb.Database, allocPath string, isBintrie bool) (*state.StateDB, error) {
+ tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie})
+ sdb := state.NewDatabase(tdb, nil)
+
+ root := types.EmptyRootHash
+ if isBintrie {
+ root = types.EmptyBinaryHash
+ }
+ statedb, err := state.New(root, sdb)
+ if err != nil {
+ return nil, NewError(ErrorEVM, fmt.Errorf("failed to create initial statedb: %v", err))
+ }
+
+ f, err := os.Open(allocPath)
+ if err != nil {
+ return nil, NewError(ErrorIO, fmt.Errorf("failed reading alloc file: %v", err))
+ }
+ defer f.Close()
+
+ dec := json.NewDecoder(f)
+ tok, err := dec.Token()
+ if err != nil {
+ return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc opening token: %v", err))
+ }
+ if d, ok := tok.(json.Delim); !ok || d != '{' {
+ return nil, NewError(ErrorJson, fmt.Errorf("expected alloc object, got %v", tok))
+ }
+ for dec.More() {
+ keyTok, err := dec.Token()
+ if err != nil {
+ return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc key: %v", err))
+ }
+ keyStr, ok := keyTok.(string)
+ if !ok {
+ return nil, NewError(ErrorJson, fmt.Errorf("alloc key not a string: %v", keyTok))
+ }
+ addr := common.HexToAddress(keyStr)
+ var acct types.Account
+ if err := dec.Decode(&acct); err != nil {
+ return nil, NewError(ErrorJson, fmt.Errorf("failed decoding account %s: %v", keyStr, err))
+ }
+ statedb.SetCode(addr, acct.Code, tracing.CodeChangeUnspecified)
+ statedb.SetNonce(addr, acct.Nonce, tracing.NonceChangeGenesis)
+ if acct.Balance != nil {
+ statedb.SetBalance(addr, uint256.MustFromBig(acct.Balance), tracing.BalanceIncreaseGenesisBalance)
+ }
+ for k, v := range acct.Storage {
+ statedb.SetState(addr, k, v)
+ }
+ }
+ if _, err := dec.Token(); err != nil {
+ return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc closing token: %v", err))
+ }
+
+ root, err = statedb.Commit(0, false, false)
+ if err != nil {
+ return nil, NewError(ErrorEVM, fmt.Errorf("failed to commit initial state: %v", err))
+ }
+ if isBintrie {
+ return statedb, nil
+ }
+ statedb, err = state.New(root, sdb)
+ if err != nil {
+ return nil, NewError(ErrorEVM, fmt.Errorf("failed to reopen state after commit: %v", err))
+ }
+ return statedb, nil
+}
+
func rlpHash(x any) (h common.Hash) {
hw := keccak.NewLegacyKeccak256()
rlp.Encode(hw, x)
diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go
index 3a457eeaec..ca19ae3386 100644
--- a/cmd/evm/internal/t8ntool/transaction.go
+++ b/cmd/evm/internal/t8ntool/transaction.go
@@ -133,21 +133,21 @@ func Transaction(ctx *cli.Context) error {
}
// Check intrinsic gas
rules := chainConfig.Rules(common.Big0, true, 0)
- gas, 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)
continue
}
- r.IntrinsicGas = gas
- if tx.Gas() < gas {
- r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), gas)
+ r.IntrinsicGas = cost.RegularGas
+ if tx.Gas() < cost.RegularGas {
+ r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), cost.RegularGas)
results = append(results, r)
continue
}
// For Prague txs, validate the floor data gas.
if rules.IsPrague {
- floorDataGas, err := core.FloorDataGas(tx.Data())
+ floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
r.Error = err
results = append(results, r)
@@ -185,7 +185,9 @@ func Transaction(ctx *cli.Context) error {
}
}
- if chainConfig.IsOsaka(new(big.Int), 0) && tx.Gas() > params.MaxTxGas {
+ isOsaka := chainConfig.IsOsaka(new(big.Int), 0)
+ isAmsterdam := chainConfig.IsAmsterdam(new(big.Int), 0)
+ if isOsaka && !isAmsterdam && tx.Gas() > params.MaxTxGas {
r.Error = errors.New("gas limit exceeds maximum")
}
results = append(results, r)
diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index d7cdc98e74..89b703d3b8 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -17,6 +17,7 @@
package t8ntool
import (
+ "bufio"
"encoding/json"
"errors"
"fmt"
@@ -115,11 +116,10 @@ func Transition(ctx *cli.Context) error {
}
}
if allocStr != stdinSelector {
- if err := readFile(allocStr, "alloc", &inputData.Alloc); err != nil {
- return err
- }
+ prestate.AllocPath = allocStr
+ } else {
+ prestate.Pre = inputData.Alloc
}
- prestate.Pre = inputData.Alloc
if btStr != stdinSelector && btStr != "" {
if err := readFile(btStr, "BT", &inputData.BT); err != nil {
@@ -223,22 +223,57 @@ func Transition(ctx *cli.Context) error {
return err
}
}
- // Dump the execution result
+ // Dump the execution result.
var (
- collector = make(Alloc)
+ collector Alloc
btleaves map[common.Hash]hexutil.Bytes
)
- isBinary := chainConfig.IsVerkle(big.NewInt(int64(prestate.Env.Number)), prestate.Env.Timestamp)
- if !isBinary {
+ isBinary := chainConfig.IsUBT(big.NewInt(int64(prestate.Env.Number)), prestate.Env.Timestamp)
+ allocOutput := ctx.String(OutputAllocFlag.Name)
+ switch {
+ case !isBinary && allocOutput != "" && allocOutput != "stdout" && allocOutput != "stderr":
+ // Stream directly to the output file to avoid materializing the
+ // whole post-state in memory. dispatchOutput is told to skip alloc
+ // by clearing the output name.
+ if err := writeStreamedAlloc(filepath.Join(baseDir, allocOutput), s); err != nil {
+ return err
+ }
+ allocOutput = ""
+ case !isBinary:
+ collector = make(Alloc)
s.DumpToCollector(collector, nil)
- } else {
+ default:
btleaves = make(map[common.Hash]hexutil.Bytes)
if err := s.DumpBinTrieLeaves(btleaves); err != nil {
return err
}
}
+ return dispatchOutput(ctx, baseDir, result, collector, allocOutput, body, btleaves)
+}
- return dispatchOutput(ctx, baseDir, result, collector, body, btleaves)
+// writeStreamedAlloc writes the post-state alloc to path one account at a
+// time, producing the same JSON shape as saveFile on an Alloc map.
+func writeStreamedAlloc(path string, s *state.StateDB) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return NewError(ErrorIO, fmt.Errorf("failed creating alloc output file: %v", err))
+ }
+ bw := bufio.NewWriter(f)
+ sa := newStreamingAlloc(bw)
+ s.DumpToCollector(sa, nil)
+ if err := sa.Close(); err != nil {
+ f.Close()
+ return NewError(ErrorIO, fmt.Errorf("failed writing alloc output: %v", err))
+ }
+ if err := bw.Flush(); err != nil {
+ f.Close()
+ return NewError(ErrorIO, fmt.Errorf("failed flushing alloc output: %v", err))
+ }
+ if err := f.Close(); err != nil {
+ return NewError(ErrorIO, fmt.Errorf("failed closing alloc output file: %v", err))
+ }
+ log.Info("Wrote file", "file", path)
+ return nil
}
func applyLondonChecks(env *stEnv, chainConfig *params.ChainConfig) error {
@@ -327,6 +362,10 @@ func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) {
if addr == nil {
return
}
+ g[*addr] = dumpAccountToTypesAccount(dumpAccount)
+}
+
+func dumpAccountToTypesAccount(dumpAccount state.DumpAccount) types.Account {
balance, _ := new(big.Int).SetString(dumpAccount.Balance, 0)
var storage map[common.Hash]common.Hash
if dumpAccount.Storage != nil {
@@ -335,13 +374,64 @@ func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) {
storage[k] = common.HexToHash(v)
}
}
- genesisAccount := types.Account{
+ return types.Account{
Code: dumpAccount.Code,
Storage: storage,
Balance: balance,
Nonce: dumpAccount.Nonce,
}
- g[*addr] = genesisAccount
+}
+
+// streamingAlloc is a DumpCollector that writes each account to w as it is
+// visited, emitting a single JSON object keyed by address. Close must be
+// called to emit the closing brace.
+type streamingAlloc struct {
+ w io.Writer
+ wroteOne bool
+ err error
+}
+
+func newStreamingAlloc(w io.Writer) *streamingAlloc {
+ return &streamingAlloc{w: w}
+}
+
+func (s *streamingAlloc) write(b []byte) {
+ if s.err != nil {
+ return
+ }
+ _, s.err = s.w.Write(b)
+}
+
+func (s *streamingAlloc) OnRoot(common.Hash) {
+ s.write([]byte{'{'})
+}
+
+func (s *streamingAlloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) {
+ if s.err != nil || addr == nil {
+ return
+ }
+ keyJSON, err := json.Marshal(*addr)
+ if err != nil {
+ s.err = err
+ return
+ }
+ valueJSON, err := json.Marshal(dumpAccountToTypesAccount(dumpAccount))
+ if err != nil {
+ s.err = err
+ return
+ }
+ if s.wroteOne {
+ s.write([]byte{','})
+ }
+ s.write(keyJSON)
+ s.write([]byte{':'})
+ s.write(valueJSON)
+ s.wroteOne = true
+}
+
+func (s *streamingAlloc) Close() error {
+ s.write([]byte{'}'})
+ return s.err
}
// saveFile marshals the object to the given file
@@ -359,8 +449,9 @@ func saveFile(baseDir, filename string, data interface{}) error {
}
// dispatchOutput writes the output data to either stderr or stdout, or to the specified
-// files
-func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, body hexutil.Bytes, bt map[common.Hash]hexutil.Bytes) error {
+// files. An empty allocOutput skips the alloc dispatch, which is used when the
+// alloc has already been streamed to disk by the caller.
+func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, allocOutput string, body hexutil.Bytes, bt map[common.Hash]hexutil.Bytes) error {
stdOutObject := make(map[string]interface{})
stdErrObject := make(map[string]interface{})
dispatch := func(baseDir, fName, name string, obj interface{}) error {
@@ -378,7 +469,7 @@ func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, a
}
return nil
}
- if err := dispatch(baseDir, ctx.String(OutputAllocFlag.Name), "alloc", alloc); err != nil {
+ if err := dispatch(baseDir, allocOutput, "alloc", alloc); err != nil {
return err
}
if err := dispatch(baseDir, ctx.String(OutputResultFlag.Name), "result", result); err != nil {
@@ -452,10 +543,10 @@ func BinKeys(ctx *cli.Context) error {
return err
}
}
- db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
+ 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)
}
@@ -496,10 +587,10 @@ func BinTrieRoot(ctx *cli.Context) error {
return err
}
}
- db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
+ 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)
}
@@ -509,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/main.go b/cmd/evm/main.go
index 77a06bec26..2b77741738 100644
--- a/cmd/evm/main.go
+++ b/cmd/evm/main.go
@@ -115,7 +115,7 @@ var (
Name: "trace.noreturndata",
Aliases: []string{"noreturndata"},
Value: true,
- Usage: "enable return data output",
+ Usage: "disable return data output",
Category: traceCategory,
}
diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go
index ebb3e04461..6d80056d04 100644
--- a/cmd/evm/runner.go
+++ b/cmd/evm/runner.go
@@ -166,8 +166,11 @@ func timedExec(bench bool, execFunc func() ([]byte, uint64, error)) ([]byte, exe
if haveGasUsed != gasUsed {
panic(fmt.Sprintf("gas differs, have %v want %v", haveGasUsed, gasUsed))
}
- if haveErr != err {
- panic(fmt.Sprintf("err differs, have %v want %v", haveErr, err))
+ if (haveErr == nil) != (err == nil) {
+ panic(fmt.Sprintf("err differs in nil-ness, have %v want %v", haveErr, err))
+ }
+ if haveErr != nil && err != nil && haveErr.Error() != err.Error() {
+ panic(fmt.Sprintf("err differs, have %q want %q", haveErr.Error(), err.Error()))
}
}
})
@@ -318,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 3730768697..46cb3aa7e4 100644
--- a/cmd/geth/bintrie_convert.go
+++ b/cmd/geth/bintrie_convert.go
@@ -144,14 +144,14 @@ func convertToBinaryTrie(ctx *cli.Context) error {
defer srcTriedb.Close()
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
- IsVerkle: true,
+ IsUBT: true,
PathDB: &pathdb.Config{
JournalDirectory: stack.ResolvePath("triedb-bintrie"),
},
})
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 9b95f6a70f..32e8c7e55b 100644
--- a/cmd/geth/bintrie_convert_test.go
+++ b/cmd/geth/bintrie_convert_test.go
@@ -82,12 +82,12 @@ func TestBintrieConvert(t *testing.T) {
defer srcTriedb2.Close()
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
- IsVerkle: true,
- PathDB: pathdb.Defaults,
+ IsUBT: true,
+ PathDB: pathdb.Defaults,
})
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)
}
@@ -190,11 +190,11 @@ func TestBintrieConvertDeleteSource(t *testing.T) {
})
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
- IsVerkle: true,
- PathDB: pathdb.Defaults,
+ IsUBT: true,
+ 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 1084100f39..98ed348d8c 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -64,7 +64,7 @@ var (
utils.OverrideOsaka,
utils.OverrideBPO1,
utils.OverrideBPO2,
- utils.OverrideVerkle,
+ utils.OverrideUBT,
}, utils.DatabaseFlags),
Description: `
The init command initializes a new genesis block and definition for the network.
@@ -297,15 +297,15 @@ func initGenesis(ctx *cli.Context) error {
v := ctx.Uint64(utils.OverrideBPO2.Name)
overrides.OverrideBPO2 = &v
}
- if ctx.IsSet(utils.OverrideVerkle.Name) {
- v := ctx.Uint64(utils.OverrideVerkle.Name)
- overrides.OverrideVerkle = &v
+ if ctx.IsSet(utils.OverrideUBT.Name) {
+ v := ctx.Uint64(utils.OverrideUBT.Name)
+ overrides.OverrideUBT = &v
}
chaindb := utils.MakeChainDatabase(ctx, stack, false)
defer chaindb.Close()
- triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle())
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsUBT())
defer triedb.Close()
_, hash, compatErr, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides, nil)
@@ -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 720d1ef9fc..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"
@@ -235,9 +236,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
v := ctx.Uint64(utils.OverrideBPO2.Name)
cfg.Eth.OverrideBPO2 = &v
}
- if ctx.IsSet(utils.OverrideVerkle.Name) {
- v := ctx.Uint64(utils.OverrideVerkle.Name)
- cfg.Eth.OverrideVerkle = &v
+ if ctx.IsSet(utils.OverrideUBT.Name) {
+ v := ctx.Uint64(utils.OverrideUBT.Name)
+ cfg.Eth.OverrideUBT = &v
}
// Start metrics export if enabled.
@@ -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/dbcmd.go b/cmd/geth/dbcmd.go
index 455dd05aca..173fc97def 100644
--- a/cmd/geth/dbcmd.go
+++ b/cmd/geth/dbcmd.go
@@ -806,6 +806,24 @@ func (iter *snapshotIterator) Release() {
iter.storage.Release()
}
+type codeIterator struct {
+ iter ethdb.Iterator
+}
+
+func (iter *codeIterator) Next() (byte, []byte, []byte, bool) {
+ for iter.iter.Next() {
+ key := iter.iter.Key()
+ if bytes.HasPrefix(key, rawdb.CodePrefix) && len(key) == (len(rawdb.CodePrefix)+common.HashLength) {
+ return utils.OpBatchAdd, key, iter.iter.Value(), true
+ }
+ }
+ return 0, nil, nil, false
+}
+
+func (iter *codeIterator) Release() {
+ iter.iter.Release()
+}
+
// chainExporters defines the export scheme for all exportable chain data.
var chainExporters = map[string]func(db ethdb.Database) utils.ChainDataIterator{
"preimage": func(db ethdb.Database) utils.ChainDataIterator {
@@ -817,6 +835,10 @@ var chainExporters = map[string]func(db ethdb.Database) utils.ChainDataIterator{
storage := db.NewIterator(rawdb.SnapshotStoragePrefix, nil)
return &snapshotIterator{account: account, storage: storage}
},
+ "code": func(db ethdb.Database) utils.ChainDataIterator {
+ iter := db.NewIterator(rawdb.CodePrefix, nil)
+ return &codeIterator{iter: iter}
+ },
}
func exportChaindata(ctx *cli.Context) error {
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index e196ac8688..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"
@@ -64,7 +61,7 @@ var (
utils.OverrideOsaka,
utils.OverrideBPO1,
utils.OverrideBPO2,
- utils.OverrideVerkle,
+ utils.OverrideUBT,
utils.OverrideGenesisFlag,
utils.EnablePersonal, // deprecated
utils.TxPoolLocalsFlag,
@@ -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/geth/snapshot.go b/cmd/geth/snapshot.go
index c177fb5ea2..d168ee1d7d 100644
--- a/cmd/geth/snapshot.go
+++ b/cmd/geth/snapshot.go
@@ -18,15 +18,18 @@ package main
import (
"bytes"
+ "encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"slices"
+ "sort"
"time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/pruner"
@@ -168,6 +171,22 @@ block is used.
Description: `
The export-preimages command exports hash preimages to a flat file, in exactly
the expected order for the overlay tree migration.
+`,
+ },
+ {
+ Name: "list-eip-7610-accounts",
+ Aliases: []string{"eip7610"},
+ Usage: "list EIP7610 eligible accounts",
+ Action: listEIP7610EligibleAccounts,
+ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
+ Description: `
+geth snapshot list-eip-7610-accounts
+traverses the post–EIP-161 state and returns all accounts that are eligible
+under EIP-7610: accounts with zero nonce, empty runtime code, and non-empty
+storage. The traversal will be aborted immediately if the state is prior to
+EIP-161.
+
+The exported accounts are identified by their address.
`,
},
},
@@ -801,3 +820,92 @@ func checkAccount(ctx *cli.Context) error {
log.Info("Checked the snapshot journalled storage", "time", common.PrettyDuration(time.Since(start)))
return nil
}
+
+// listEIP7610EligibleAccounts traverses the post–EIP-161 state and returns all
+// accounts that are eligible under EIP-7610: accounts with zero nonce, empty
+// runtime code, and non-empty storage.
+//
+// Such accounts could only have been created before EIP-161, since after that
+// all newly created contracts are initialized with a nonce of one.
+//
+// This helper should be generally applicable to all networks, including the
+// Ethereum mainnet. For most networks where EIP-161 was enabled from genesis,
+// the resulting set is expected to be empty. Otherwise, network operators are
+// responsible for generating the eligible account set themselves.
+//
+// Notably, the exported accounts are identified by their address.
+func listEIP7610EligibleAccounts(ctx *cli.Context) error {
+ stack, _ := makeConfigNode(ctx)
+ defer stack.Close()
+
+ chaindb := utils.MakeChainDatabase(ctx, stack, true)
+ defer chaindb.Close()
+
+ headBlock := rawdb.ReadHeadBlock(chaindb)
+ if headBlock == nil {
+ log.Error("Failed to load head block")
+ return nil
+ }
+ config, _, err := core.LoadChainConfig(chaindb, utils.MakeGenesis(ctx))
+ if err != nil {
+ log.Error("Failed to load chain config", "err", err)
+ return err
+ }
+ if !config.IsEIP158(headBlock.Number()) {
+ log.Info("Local head is prior to EIP-161", "head", headBlock.Number(), "eip-161", *config.EIP158Block)
+ return nil
+ }
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false)
+ defer triedb.Close()
+
+ if triedb.Scheme() != rawdb.PathScheme {
+ log.Error("Hash scheme is not supported")
+ return nil
+ }
+ iter, err := triedb.AccountIterator(headBlock.Root(), common.Hash{})
+ if err != nil {
+ log.Error("Failed to get account iterator", "err", err)
+ return err
+ }
+ var (
+ start = time.Now()
+ accounts []common.Address
+ )
+ for iter.Next() {
+ blob := iter.Account()
+ if blob == nil {
+ log.Error("Failed to get account blob")
+ return nil
+ }
+ var account types.SlimAccount
+ if err := rlp.DecodeBytes(blob, &account); err != nil {
+ log.Error("Failed to decode", "err", err)
+ return err
+ }
+ // EIP-7610 account eligibility:
+ // - account.nonce == 0
+ // - account.runtime_code == empty
+ // - account.storage != empty
+ if len(account.CodeHash) == 0 && account.Nonce == 0 && len(account.Root) != 0 {
+ preimage := rawdb.ReadPreimage(chaindb, iter.Hash())
+ if preimage == nil {
+ log.Error("Failed to read preimage", "hash", iter.Hash().Hex())
+ return nil
+ }
+ accounts = append(accounts, common.BytesToAddress(preimage))
+ }
+ }
+ if len(accounts) == 0 {
+ log.Info("Traversed state", "eligible", len(accounts), "elapsed", common.PrettyDuration(time.Since(start)))
+ } else {
+ sort.Slice(accounts, func(i, j int) bool {
+ return accounts[i].Cmp(accounts[j]) < 0
+ })
+ buf := make([]byte, len(accounts)*common.AddressLength)
+ for i, h := range accounts {
+ copy(buf[i*common.AddressLength:], h[:])
+ }
+ log.Info("Traversed state", "eligible", len(accounts), "elapsed", common.PrettyDuration(time.Since(start)), "output", hex.EncodeToString(buf))
+ }
+ return nil
+}
diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod
index 8303b4ab2e..2d99cb2232 100644
--- a/cmd/keeper/go.mod
+++ b/cmd/keeper/go.mod
@@ -38,8 +38,8 @@ require (
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
- golang.org/x/crypto v0.44.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum
index a162537c88..09c8e55822 100644
--- a/cmd/keeper/go.sum
+++ b/cmd/keeper/go.sum
@@ -127,20 +127,20 @@ go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ7
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index c1284044eb..c41cf4ee40 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -264,9 +264,9 @@ var (
Usage: "Manually specify the bpo2 fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
- OverrideVerkle = &cli.Uint64Flag{
- Name: "override.verkle",
- Usage: "Manually specify the Verkle fork timestamp, overriding the bundled setting",
+ OverrideUBT = &cli.Uint64Flag{
+ Name: "override.ubt",
+ Usage: "Manually specify the UBT fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideGenesisFlag = &cli.StringFlag{
@@ -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)",
@@ -1067,19 +1073,19 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
RPCTelemetryEndpointFlag = &cli.StringFlag{
Name: "rpc.telemetry.endpoint",
- Usage: "Defines where RPC telemetry is sent (e.g., http://localhost:4318)",
+ Usage: "Defines where RPC telemetry is sent (e.g., http://localhost:4318 or grpc://localhost:4317)",
Category: flags.APICategory,
}
RPCTelemetryUserFlag = &cli.StringFlag{
Name: "rpc.telemetry.username",
- Usage: "HTTP Basic Auth username for OpenTelemetry",
+ Usage: "Basic Auth username for OpenTelemetry",
Category: flags.APICategory,
}
RPCTelemetryPasswordFlag = &cli.StringFlag{
Name: "rpc.telemetry.password",
- Usage: "HTTP Basic Auth password for OpenTelemetry",
+ Usage: "Basic Auth password for OpenTelemetry",
Category: flags.APICategory,
}
@@ -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)
}
@@ -1899,7 +1908,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
cfg.StatelessSelfValidation = ctx.Bool(VMStatelessSelfValidationFlag.Name)
}
// Auto-enable StatelessSelfValidation when witness stats are enabled
- if ctx.Bool(VMWitnessStatsFlag.Name) {
+ if cfg.EnableWitnessStats {
cfg.StatelessSelfValidation = true
}
@@ -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,
@@ -2516,10 +2526,10 @@ func MakeConsolePreloads(ctx *cli.Context) []string {
}
// MakeTrieDatabase constructs a trie database based on the configured scheme.
-func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, preimage bool, readOnly bool, isVerkle bool) *triedb.Database {
+func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, preimage bool, readOnly bool, isUBT bool) *triedb.Database {
config := &triedb.Config{
Preimages: preimage,
- IsVerkle: isVerkle,
+ IsUBT: isUBT,
}
scheme, err := rawdb.ParseStateScheme(ctx.String(StateSchemeFlag.Name), disk)
if err != nil {
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 c4a284d485..72ac75c036 100644
--- a/consensus/beacon/consensus.go
+++ b/consensus/beacon/consensus.go
@@ -17,7 +17,6 @@
package beacon
import (
- "context"
"errors"
"fmt"
"math/big"
@@ -26,13 +25,10 @@ import (
"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/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
- "github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/params"
- "github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
)
@@ -361,48 +357,6 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
// No block reward which is issued by consensus layer instead.
}
-// FinalizeAndAssemble implements consensus.Engine, setting the final state and
-// assembling the block.
-func (beacon *Beacon) FinalizeAndAssemble(ctx context.Context, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (result *types.Block, err error) {
- ctx, _, spanEnd := telemetry.StartSpan(ctx, "consensus.beacon.FinalizeAndAssemble",
- telemetry.Int64Attribute("block.number", int64(header.Number.Uint64())),
- telemetry.Int64Attribute("txs.count", int64(len(body.Transactions))),
- telemetry.Int64Attribute("withdrawals.count", int64(len(body.Withdrawals))),
- )
- defer spanEnd(&err)
-
- if !beacon.IsPoSHeader(header) {
- block, delegateErr := beacon.ethone.FinalizeAndAssemble(ctx, chain, header, state, body, receipts)
- return block, delegateErr
- }
- shanghai := chain.Config().IsShanghai(header.Number, header.Time)
- if shanghai {
- // All blocks after Shanghai must include a withdrawals root.
- if body.Withdrawals == nil {
- body.Withdrawals = make([]*types.Withdrawal, 0)
- }
- } else {
- if len(body.Withdrawals) > 0 {
- return nil, errors.New("withdrawals set before Shanghai activation")
- }
- }
- // Finalize and assemble the block.
- _, _, finalizeSpanEnd := telemetry.StartSpan(ctx, "consensus.beacon.Finalize")
- beacon.Finalize(chain, header, state, body)
- finalizeSpanEnd(nil)
-
- // Assign the final state root to header.
- _, _, rootSpanEnd := telemetry.StartSpan(ctx, "consensus.beacon.IntermediateRoot")
- header.Root = state.IntermediateRoot(true)
- rootSpanEnd(nil)
-
- // Assemble the final block.
- _, _, blockSpanEnd := telemetry.StartSpan(ctx, "consensus.beacon.NewBlock")
- block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
- blockSpanEnd(nil)
- return block, nil
-}
-
// Seal generates a new sealing request for the given input block and pushes
// the result into the given channel.
//
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index 3bf79d5a62..ceaec44656 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -19,7 +19,6 @@ package clique
import (
"bytes"
- "context"
"errors"
"fmt"
"io"
@@ -34,7 +33,6 @@ import (
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
- "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/crypto"
@@ -43,7 +41,6 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
- "github.com/ethereum/go-ethereum/trie"
)
const (
@@ -580,22 +577,6 @@ func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Heade
// No block rewards in PoA, so the state remains as is
}
-// FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set,
-// nor block rewards given, and returns the final block.
-func (c *Clique) FinalizeAndAssemble(ctx context.Context, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
- if len(body.Withdrawals) > 0 {
- return nil, errors.New("clique does not support withdrawals")
- }
- // Finalize block
- c.Finalize(chain, header, state, body)
-
- // Assign the final state root to header.
- header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
-
- // Assemble and return the final block for sealing.
- return types.NewBlock(header, &types.Body{Transactions: body.Transactions}, receipts, trie.NewStackTrie(nil)), nil
-}
-
// Authorize injects a private key into the consensus engine to mint new blocks
// with.
func (c *Clique) Authorize(signer common.Address) {
diff --git a/consensus/consensus.go b/consensus/consensus.go
index 094026b614..4ba389292f 100644
--- a/consensus/consensus.go
+++ b/consensus/consensus.go
@@ -18,11 +18,9 @@
package consensus
import (
- "context"
"math/big"
"github.com/ethereum/go-ethereum/common"
- "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/params"
@@ -88,13 +86,6 @@ type Engine interface {
// that happen at finalization (e.g. block rewards).
Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body)
- // FinalizeAndAssemble runs any post-transaction state modifications (e.g. block
- // rewards or process withdrawals) and assembles the final block.
- //
- // Note: The block header and state database might be updated to reflect any
- // consensus rules that happen at finalization (e.g. block rewards).
- FinalizeAndAssemble(ctx context.Context, chain ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error)
-
// 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 56256d1215..ee9d9d97d6 100644
--- a/consensus/ethash/consensus.go
+++ b/consensus/ethash/consensus.go
@@ -17,7 +17,6 @@
package ethash
import (
- "context"
"errors"
"fmt"
"math/big"
@@ -28,14 +27,12 @@ import (
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
- "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/vm"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
- "github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
)
@@ -512,22 +509,6 @@ func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.
accumulateRewards(chain.Config(), state, header, body.Uncles)
}
-// FinalizeAndAssemble implements consensus.Engine, accumulating the block and
-// uncle rewards, setting the final state and assembling the block.
-func (ethash *Ethash) FinalizeAndAssemble(ctx context.Context, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
- if len(body.Withdrawals) > 0 {
- return nil, errors.New("ethash does not support withdrawals")
- }
- // Finalize block
- ethash.Finalize(chain, header, state, body)
-
- // Assign the final state root to header.
- header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
-
- // Header seems complete, assemble into a block and return
- return types.NewBlock(header, &types.Body{Transactions: body.Transactions, Uncles: body.Uncles}, receipts, trie.NewStackTrie(nil)), nil
-}
-
// SealHash returns the hash of a block prior to it being sealed.
func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) {
hasher := keccak.NewLegacyKeccak256()
diff --git a/core/bench_test.go b/core/bench_test.go
index 932188f8e2..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{}
- gas, _ := 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 {
@@ -99,7 +99,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
Nonce: gen.TxNonce(benchRootAddr),
To: &toaddr,
Value: big.NewInt(1),
- Gas: gas,
+ Gas: cost.RegularGas,
Data: data,
GasPrice: gasPrice,
})
diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go
index 7704ba41fb..5f6239e4fa 100644
--- a/core/bintrie_witness_test.go
+++ b/core/bintrie_witness_test.go
@@ -36,7 +36,7 @@ import (
)
var (
- testVerkleChainConfig = ¶ms.ChainConfig{
+ testUBTChainConfig = ¶ms.ChainConfig{
ChainID: big.NewInt(1),
HomesteadBlock: big.NewInt(0),
EIP150Block: big.NewInt(0),
@@ -51,30 +51,30 @@ var (
LondonBlock: big.NewInt(0),
Ethash: new(params.EthashConfig),
ShanghaiTime: u64(0),
- VerkleTime: u64(0),
+ UBTTime: u64(0),
TerminalTotalDifficulty: common.Big0,
- EnableVerkleAtGenesis: true,
+ EnableUBTAtGenesis: true,
BlobScheduleConfig: ¶ms.BlobScheduleConfig{
- Verkle: params.DefaultPragueBlobConfig,
+ UBT: params.DefaultPragueBlobConfig,
},
}
)
-func TestProcessVerkle(t *testing.T) {
+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)
- signer = types.LatestSigner(testVerkleChainConfig)
+ 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
coinbase = common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7")
gspec = &Genesis{
- Config: testVerkleChainConfig,
+ Config: testUBTChainConfig,
Alloc: GenesisAlloc{
coinbase: {
Balance: big.NewInt(1000000000000000000), // 1 ether
@@ -87,21 +87,22 @@ func TestProcessVerkle(t *testing.T) {
},
}
)
- // Verkle trees use the snapshot, which must be enabled before the
+ // UBTs use the snapshot, which must be enabled before the
// data is saved into the tree+database.
// 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()
txCost1 := params.TxGas
txCost2 := params.TxGas
- contractCreationCost := intrinsicContractCreationGas +
+ contractCreationCost := intrinsicContractCreationGas.RegularGas +
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* creation with value */
739 /* execution costs */
- codeWithExtCodeCopyGas := intrinsicCodeWithExtCodeCopyGas +
+ codeWithExtCodeCopyGas := intrinsicCodeWithExtCodeCopyGas.RegularGas +
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (tx) */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + params.WitnessBranchReadCost + params.WitnessBranchWriteCost + /* creation (CREATE at pc=0x20) */
params.WitnessChunkReadCost + params.WitnessChunkWriteCost + /* write code hash */
@@ -188,7 +189,7 @@ func TestProcessParentBlockHash(t *testing.T) {
// block 1 parent hash is 0x0100....
// block 2 parent hash is 0x0200....
// etc
- checkBlockHashes := func(statedb *state.StateDB, isVerkle bool) {
+ checkBlockHashes := func(statedb *state.StateDB, isUBT bool) {
statedb.SetNonce(params.HistoryStorageAddress, 1, tracing.NonceChangeUnspecified)
statedb.SetCode(params.HistoryStorageAddress, params.HistoryStorageCode, tracing.CodeChangeUnspecified)
// Process n blocks, from 1 .. num
@@ -196,8 +197,8 @@ func TestProcessParentBlockHash(t *testing.T) {
for i := 1; i <= num; i++ {
header := &types.Header{ParentHash: common.Hash{byte(i)}, Number: big.NewInt(int64(i)), Difficulty: new(big.Int)}
chainConfig := params.MergedTestChainConfig
- if isVerkle {
- chainConfig = testVerkleChainConfig
+ if isUBT {
+ chainConfig = testUBTChainConfig
}
vmContext := NewEVMBlockContext(header, nil, new(common.Address))
evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
@@ -205,9 +206,9 @@ func TestProcessParentBlockHash(t *testing.T) {
}
// Read block hashes for block 0 .. num-1
for i := 0; i < num; i++ {
- have, want := getContractStoredBlockHash(statedb, uint64(i), isVerkle), common.Hash{byte(i + 1)}
+ have, want := getContractStoredBlockHash(statedb, uint64(i), isUBT), common.Hash{byte(i + 1)}
if have != want {
- t.Errorf("block %d, verkle=%v, have parent hash %v, want %v", i, isVerkle, have, want)
+ t.Errorf("block %d, verkle=%v, have parent hash %v, want %v", i, isUBT, have, want)
}
}
}
@@ -215,22 +216,23 @@ func TestProcessParentBlockHash(t *testing.T) {
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
checkBlockHashes(statedb, false)
})
- t.Run("Verkle", func(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.EmptyVerkleHash, state.NewDatabase(triedb, nil))
+ statedb, _ := state.New(types.EmptyBinaryHash, state.NewDatabase(triedb, nil))
checkBlockHashes(statedb, true)
})
}
// getContractStoredBlockHash is a utility method which reads the stored parent blockhash for block 'number'
-func getContractStoredBlockHash(statedb *state.StateDB, number uint64, isVerkle bool) common.Hash {
+func getContractStoredBlockHash(statedb *state.StateDB, number uint64, isUBT bool) common.Hash {
ringIndex := number % params.HistoryServeWindow
var key common.Hash
binary.BigEndian.PutUint64(key[24:], ringIndex)
- if isVerkle {
+ if isUBT {
return statedb.GetState(params.HistoryStorageAddress, key)
}
return statedb.GetState(params.HistoryStorageAddress, key)
diff --git a/core/blockchain.go b/core/blockchain.go
index f398c2fc10..2b1c1c03c4 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;
@@ -258,10 +259,11 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig {
}
// triedbConfig derives the configures for trie database.
-func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
+func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config {
config := &triedb.Config{
- Preimages: cfg.Preimages,
- IsVerkle: isVerkle,
+ Preimages: cfg.Preimages,
+ IsUBT: isUBT,
+ BinTrieGroupDepth: cfg.BinTrieGroupDepth,
}
if cfg.StateScheme == rawdb.HashScheme {
config.HashDB = &hashdb.Config{
@@ -378,7 +380,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
}
// Open trie database with provided config
- enableVerkle, err := EnableVerkleAtGenesis(db, genesis)
+ enableVerkle, err := EnableUBTAtGenesis(db, genesis)
if err != nil {
return nil, err
}
@@ -1188,6 +1190,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()))
@@ -2120,11 +2123,29 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
startTime = time.Now()
statedb *state.StateDB
interrupt atomic.Bool
- sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
+ sdb state.Database
)
defer interrupt.Store(true) // terminate the prefetch at the end
- if bc.cfg.NoPrefetch {
+ if bc.chainConfig.IsUBT(block.Number(), block.Time()) {
+ sdb = state.NewUBTDatabase(bc.triedb, bc.codedb)
+ } else {
+ sdb = state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
+ }
+ // If prefetching is enabled, run that against the current state to pre-cache
+ // transactions and probabilistically some of the account/storage trie nodes.
+ //
+ // Note: the main processor and prefetcher share the same reader with a local
+ // cache for mitigating the overhead of state access.
+ type prewarmReader interface {
+ // ReadersWithCacheStats creates a pair of state readers that share the
+ // same underlying state reader and internal state cache, while maintaining
+ // separate statistics respectively.
+ ReadersWithCacheStats(stateRoot common.Hash) (state.Reader, state.Reader, error)
+ }
+ warmer, ok := sdb.(prewarmReader)
+
+ if bc.cfg.NoPrefetch || !ok {
statedb, err = state.New(parentRoot, sdb)
if err != nil {
return nil, err
@@ -2135,7 +2156,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
//
// Note: the main processor and prefetcher share the same reader with a local
// cache for mitigating the overhead of state access.
- prefetch, process, err := sdb.ReadersWithCacheStats(parentRoot)
+ prefetch, process, err := warmer.ReadersWithCacheStats(parentRoot)
if err != nil {
return nil, err
}
@@ -2580,8 +2601,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/blockchain_reader.go b/core/blockchain_reader.go
index 3614702d1a..18afa9ce9d 100644
--- a/core/blockchain_reader.go
+++ b/core/blockchain_reader.go
@@ -416,19 +416,42 @@ func (bc *BlockChain) ContractCodeWithPrefix(hash common.Hash) []byte {
// State returns a new mutable state based on the current HEAD block.
func (bc *BlockChain) State() (*state.StateDB, error) {
- return bc.StateAt(bc.CurrentBlock().Root)
+ return bc.StateAt(bc.CurrentBlock())
}
// StateAt returns a new mutable state based on a particular point in time.
-func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
- return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
+func (bc *BlockChain) StateAt(header *types.Header) (*state.StateDB, error) {
+ if bc.chainConfig.IsUBT(header.Number, header.Time) {
+ return state.New(header.Root, state.NewUBTDatabase(bc.triedb, bc.codedb))
+ }
+ return state.New(header.Root, state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
}
-// HistoricState returns a historic state specified by the given root.
+// StateAtForkBoundary returns a new mutable state based on the parent state
+// and the given header, handling the transition across the UBT fork.
+func (bc *BlockChain) StateAtForkBoundary(parent *types.Header, header *types.Header) (*state.StateDB, error) {
+ // The parent is already in the UBT fork.
+ if bc.chainConfig.IsUBT(parent.Number, parent.Time) {
+ return state.New(parent.Root, state.NewUBTDatabase(bc.triedb, bc.codedb))
+ }
+ // The current block is the first block in the UBT fork
+ // (i.e., the parent is the last MPT block).
+ if bc.chainConfig.IsUBT(header.Number, header.Time) {
+ // TODO(gballet): register chain context if needed
+ return state.New(parent.Root, state.NewUBTDatabase(bc.triedb, bc.codedb))
+ }
+ // Both the parent and current block are in the MPT fork.
+ return state.New(parent.Root, state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
+}
+
+// HistoricState returns a historic state specified by the given header.
// Live states are not available and won't be served, please use `State`
// or `StateAt` instead.
-func (bc *BlockChain) HistoricState(root common.Hash) (*state.StateDB, error) {
- return state.New(root, state.NewHistoricDatabase(bc.triedb, bc.codedb))
+func (bc *BlockChain) HistoricState(header *types.Header) (*state.StateDB, error) {
+ if bc.chainConfig.IsUBT(header.Number, header.Time) {
+ return nil, errors.New("historical state over ubt is not yet supported")
+ }
+ return state.New(header.Root, state.NewHistoricDatabase(bc.triedb, bc.codedb))
}
// Config retrieves the chain's fork configuration.
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index b7abc7fee5..1763ee74e0 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -3890,7 +3890,7 @@ func TestTransientStorageReset(t *testing.T) {
t.Fatalf("failed to insert into chain: %v", err)
}
// Check the storage
- state, err := chain.StateAt(chain.CurrentHeader().Root)
+ state, err := chain.StateAt(chain.CurrentHeader())
if err != nil {
t.Fatalf("Failed to load state %v", err)
}
diff --git a/core/chain_makers.go b/core/chain_makers.go
index 8f6eed1697..cfd6302794 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -117,7 +117,7 @@ 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))
+ b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1))
receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx)
if err != nil {
panic(err)
@@ -126,7 +126,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
// Merge the tx-local access event into the "block-local" one, in order to collect
// all values, so that the witness can be built.
- if b.statedb.Database().TrieDB().IsVerkle() {
+ if b.statedb.Database().Type().Is(state.TypeUBT) {
b.statedb.AccessEvents().Merge(evm.AccessEvents)
}
b.txs = append(b.txs, tx)
@@ -315,28 +315,17 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) {
// off the statedb before executing the system calls.
statedb = statedb.Copy()
}
+ var blockLogs []*types.Log
+ for _, r := range b.receipts {
+ blockLogs = append(blockLogs, r.Logs...)
+ }
+ // 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{})
- 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))
- }
+ requests, 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
}
@@ -392,7 +381,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
misc.ApplyDAOHardFork(statedb)
}
- if config.IsPrague(b.header.Number, b.header.Time) || config.IsVerkle(b.header.Number, b.header.Time) {
+ if config.IsPrague(b.header.Number, b.header.Time) || config.IsUBT(b.header.Number, b.header.Time) {
// EIP-2935
blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase)
blockContext.Random = &common.Hash{} // enable post-merge instruction set
@@ -411,11 +400,22 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
b.header.RequestsHash = &reqHash
}
- body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals}
- block, err := b.engine.FinalizeAndAssemble(context.Background(), cm, b.header, statedb, &body, b.receipts)
- if err != nil {
- panic(err)
+ body := types.Body{
+ Transactions: b.txs,
+ Uncles: b.uncles,
+ Withdrawals: b.withdrawals,
}
+ if !config.IsShanghai(b.header.Number, b.header.Time) {
+ if body.Withdrawals != nil {
+ panic("unexpected withdrawal before shanghai")
+ }
+ } else {
+ if body.Withdrawals == nil {
+ body.Withdrawals = make([]*types.Withdrawal, 0)
+ }
+ }
+ // Assemble the block for delivery.
+ block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts)
// 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))
@@ -430,8 +430,8 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
// Forcibly use hash-based state scheme for retaining all nodes in disk.
var triedbConfig *triedb.Config = triedb.HashDefaults
- if config.IsVerkle(config.ChainID, 0) {
- triedbConfig = triedb.VerkleDefaults
+ if config.IsUBT(config.ChainID, 0) {
+ triedbConfig = triedb.UBTDefaults
}
triedb := triedb.NewDatabase(db, triedbConfig)
defer triedb.Close()
@@ -479,8 +479,8 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int, gen func(int, *BlockGen)) (ethdb.Database, []*types.Block, []types.Receipts) {
db := rawdb.NewMemoryDatabase()
var triedbConfig *triedb.Config = triedb.HashDefaults
- if genesis.Config != nil && genesis.Config.IsVerkle(genesis.Config.ChainID, 0) {
- triedbConfig = triedb.VerkleDefaults
+ if genesis.Config != nil && genesis.Config.IsUBT(genesis.Config.ChainID, 0) {
+ triedbConfig = triedb.UBTDefaults
}
genesisTriedb := triedb.NewDatabase(db, triedbConfig)
block, err := genesis.Commit(db, genesisTriedb, nil)
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 6edc6e6779..6a0affa52e 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -129,22 +129,23 @@ func ReadGenesis(db ethdb.Database) (*Genesis, error) {
}
// hashAlloc computes the state root according to the genesis specification.
-func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
+func hashAlloc(ga *types.GenesisAlloc, isUBT bool) (common.Hash, error) {
// If a genesis-time verkle trie is requested, create a trie config
// with the verkle trie enabled so that the tree can be initialized
// as such.
var config *triedb.Config
- if isVerkle {
+ if isUBT {
config = &triedb.Config{
- PathDB: pathdb.Defaults,
- IsVerkle: true,
+ PathDB: pathdb.Defaults,
+ IsUBT: true,
+ BinTrieGroupDepth: triedb.UBTDefaults.BinTrieGroupDepth,
}
}
// Create an ephemeral in-memory database for computing hash,
// all the derived states will be discarded to not pollute disk.
emptyRoot := types.EmptyRootHash
- if isVerkle {
- emptyRoot = types.EmptyVerkleHash
+ if isUBT {
+ emptyRoot = types.EmptyBinaryHash
}
db := rawdb.NewMemoryDatabase()
statedb, err := state.New(emptyRoot, state.NewDatabase(triedb.NewDatabase(db, config), nil))
@@ -168,8 +169,8 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
// generated states will be persisted into the given database.
func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database, tracer *tracing.Hooks) (common.Hash, error) {
emptyRoot := types.EmptyRootHash
- if triedb.IsVerkle() {
- emptyRoot = types.EmptyVerkleHash
+ if triedb.IsUBT() {
+ emptyRoot = types.EmptyBinaryHash
}
statedb, err := state.New(emptyRoot, state.NewDatabase(triedb, nil))
if err != nil {
@@ -276,10 +277,10 @@ func (e *GenesisMismatchError) Error() string {
// ChainOverrides contains the changes to chain config.
type ChainOverrides struct {
- OverrideOsaka *uint64
- OverrideBPO1 *uint64
- OverrideBPO2 *uint64
- OverrideVerkle *uint64
+ OverrideOsaka *uint64
+ OverrideBPO1 *uint64
+ OverrideBPO2 *uint64
+ OverrideUBT *uint64
}
// apply applies the chain overrides on the supplied chain config.
@@ -296,8 +297,8 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error {
if o.OverrideBPO2 != nil {
cfg.BPO2Time = o.OverrideBPO2
}
- if o.OverrideVerkle != nil {
- cfg.VerkleTime = o.OverrideVerkle
+ if o.OverrideUBT != nil {
+ cfg.UBTTime = o.OverrideUBT
}
return cfg.CheckConfigForkOrder()
}
@@ -469,15 +470,15 @@ func (g *Genesis) chainConfigOrDefault(ghash common.Hash, stored *params.ChainCo
}
}
-// IsVerkle indicates whether the state is already stored in a verkle
+// IsUBT indicates whether the state is already stored in a verkle
// tree at genesis time.
-func (g *Genesis) IsVerkle() bool {
- return g.Config.IsVerkleGenesis()
+func (g *Genesis) IsUBT() bool {
+ return g.Config.IsUBTGenesis()
}
// ToBlock returns the genesis block according to genesis specification.
func (g *Genesis) ToBlock() *types.Block {
- root, err := hashAlloc(&g.Alloc, g.IsVerkle())
+ root, err := hashAlloc(&g.Alloc, g.IsUBT())
if err != nil {
panic(err)
}
@@ -609,24 +610,24 @@ func (g *Genesis) MustCommit(db ethdb.Database, triedb *triedb.Database) *types.
return block
}
-// EnableVerkleAtGenesis indicates whether the verkle fork should be activated
+// EnableUBTAtGenesis indicates whether the verkle fork should be activated
// at genesis. This is a temporary solution only for verkle devnet testing, where
// verkle fork is activated at genesis, and the configured activation date has
// already passed.
//
// In production networks (mainnet and public testnets), verkle activation always
// occurs after the genesis block, making this function irrelevant in those cases.
-func EnableVerkleAtGenesis(db ethdb.Database, genesis *Genesis) (bool, error) {
+func EnableUBTAtGenesis(db ethdb.Database, genesis *Genesis) (bool, error) {
if genesis != nil {
if genesis.Config == nil {
return false, errGenesisNoConfig
}
- return genesis.Config.EnableVerkleAtGenesis, nil
+ return genesis.Config.EnableUBTAtGenesis, nil
}
if ghash := rawdb.ReadCanonicalHash(db, 0); ghash != (common.Hash{}) {
chainCfg := rawdb.ReadChainConfig(db, ghash)
if chainCfg != nil {
- return chainCfg.EnableVerkleAtGenesis, nil
+ return chainCfg.EnableUBTAtGenesis, nil
}
}
return false, nil
diff --git a/core/genesis_test.go b/core/genesis_test.go
index 2b08b36690..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,34 +281,34 @@ func TestVerkleGenesisCommit(t *testing.T) {
ArrowGlacierBlock: big.NewInt(0),
GrayGlacierBlock: big.NewInt(0),
MergeNetsplitBlock: nil,
- ShanghaiTime: &verkleTime,
- CancunTime: &verkleTime,
- PragueTime: &verkleTime,
- OsakaTime: &verkleTime,
- VerkleTime: &verkleTime,
+ ShanghaiTime: &ubtTime,
+ CancunTime: &ubtTime,
+ PragueTime: &ubtTime,
+ OsakaTime: &ubtTime,
+ UBTTime: &ubtTime,
TerminalTotalDifficulty: big.NewInt(0),
- EnableVerkleAtGenesis: true,
+ EnableUBTAtGenesis: true,
Ethash: nil,
Clique: nil,
BlobScheduleConfig: ¶ms.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig,
Osaka: params.DefaultOsakaBlobConfig,
- Verkle: params.DefaultPragueBlobConfig,
+ UBT: params.DefaultPragueBlobConfig,
},
}
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}}},
},
}
- expected := common.FromHex("1fd154971d9a386c4ec75fe7138c17efb569bfc2962e46e94a376ba997e3fadc")
+ expected := common.FromHex("0870fd587c41dc778019de8c5cb3193fe4ef1f417976461952d3712ba39163f5")
got := genesis.ToBlock().Root().Bytes()
if !bytes.Equal(got, expected) {
t.Fatalf("invalid genesis state root, expected %x, got %x", expected, got)
@@ -320,17 +320,18 @@ func TestVerkleGenesisCommit(t *testing.T) {
config.NoAsyncFlush = true
triedb := triedb.NewDatabase(db, &triedb.Config{
- IsVerkle: 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
- if !triedb.IsVerkle() {
- t.Fatalf("expected trie to be verkle")
+ // Test that the trie is a unified binary trie
+ if !triedb.IsUBT() {
+ 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/history/historymode.go b/core/history/historymode.go
index 1adfe014b2..2ba746e7dd 100644
--- a/core/history/historymode.go
+++ b/core/history/historymode.go
@@ -107,6 +107,10 @@ var staticPrunePoints = map[HistoryMode]map[common.Hash]*PrunePoint{
BlockNumber: 7836331,
BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"),
},
+ params.HoodiGenesisHash: {
+ BlockNumber: 60412,
+ BlockHash: common.HexToHash("0x1562792812ef418eaafc8f1f093d84d9634971e9dd6b0771302eb5b9fd4d2c46"),
+ },
},
}
diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go
index a52d9139c9..afd2bab017 100644
--- a/core/overlay/state_transition.go
+++ b/core/overlay/state_transition.go
@@ -71,7 +71,7 @@ func (ts *TransitionState) Copy() *TransitionState {
// LoadTransitionState retrieves the Verkle transition state associated with
// the given state root hash from the database.
-func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash, isVerkle bool) *TransitionState {
+func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash, isUBT bool) *TransitionState {
var ts *TransitionState
data, _ := rawdb.ReadVerkleTransitionState(db, root)
@@ -97,10 +97,10 @@ func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash, isVerkle boo
// Initialize the first transition state, with the "ended"
// field set to true if the database was created
// as a verkle database.
- log.Debug("no transition state found, starting fresh", "verkle", isVerkle)
+ log.Debug("no transition state found, starting fresh", "verkle", isUBT)
// Start with a fresh state
- ts = &TransitionState{Ended: isVerkle}
+ ts = &TransitionState{Ended: isUBT}
}
return ts
}
diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go
index 0582e842c3..987b8df392 100644
--- a/core/rawdb/accessors_chain.go
+++ b/core/rawdb/accessors_chain.go
@@ -175,7 +175,9 @@ func WriteFinalizedBlockHash(db ethdb.KeyValueWriter, hash common.Hash) {
}
// ReadLastPivotNumber retrieves the number of the last pivot block. If the node
-// full synced, the last pivot will always be nil.
+// has never attempted snap sync, the last pivot will always be nil. The marker
+// is written during snap sync and never cleared, so that a rollback past the
+// pivot can re-enable snap sync.
func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 {
data, _ := db.Get(lastPivotKey)
if len(data) == 0 {
diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go
index 280f6e1aaa..c770e89989 100644
--- a/core/rawdb/freezer_table.go
+++ b/core/rawdb/freezer_table.go
@@ -157,6 +157,7 @@ func newTable(path string, name string, readMeter, writeMeter *metrics.Meter, si
}
meta, err = openFreezerFileForReadOnly(filepath.Join(path, fmt.Sprintf("%s.meta", name)))
if err != nil {
+ index.Close()
return nil, err
}
} else {
@@ -166,6 +167,7 @@ func newTable(path string, name string, readMeter, writeMeter *metrics.Meter, si
}
meta, err = openFreezerFileForAppend(filepath.Join(path, fmt.Sprintf("%s.meta", name)))
if err != nil {
+ index.Close()
return nil, err
}
}
@@ -173,6 +175,8 @@ func newTable(path string, name string, readMeter, writeMeter *metrics.Meter, si
// is detected.
metadata, err := newMetadata(meta)
if err != nil {
+ meta.Close()
+ index.Close()
return nil, err
}
// Create the table and repair any past inconsistency
diff --git a/core/rawdb/freezer_utils.go b/core/rawdb/freezer_utils.go
index 7f1a978b63..cd5239adc0 100644
--- a/core/rawdb/freezer_utils.go
+++ b/core/rawdb/freezer_utils.go
@@ -76,6 +76,11 @@ func copyFrom(srcPath, destPath string, offset uint64, before func(f *os.File) e
// we do the final move.
src.Close()
+ // Permanently persist the content into disk
+ if err := f.Sync(); err != nil {
+ return err
+ }
+
if err := f.Close(); err != nil {
return err
}
@@ -129,6 +134,7 @@ func openFreezerFileForAppend(filename string) (*os.File, error) {
}
// Seek to end for append
if _, err = file.Seek(0, io.SeekEnd); err != nil {
+ file.Close()
return nil, err
}
return file, nil
diff --git a/core/state/database.go b/core/state/database.go
index c603e3ad7a..3b1e627f28 100644
--- a/core/state/database.go
+++ b/core/state/database.go
@@ -20,13 +20,9 @@ import (
"fmt"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/rawdb"
- "github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
- "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
- "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
@@ -34,8 +30,27 @@ import (
"github.com/ethereum/go-ethereum/triedb"
)
+// DatabaseType represents the type of trie backing the state database.
+type DatabaseType int
+
+const (
+ // TypeMPT indicates a Merkle Patricia Trie (MPT) backed database.
+ TypeMPT DatabaseType = iota
+
+ // TypeUBT indicates a Unified Binary Trie (UBT) backed database.
+ TypeUBT
+)
+
+// Is returns the flag indicating the database type equals to the given one.
+func (typ DatabaseType) Is(t DatabaseType) bool {
+ return typ == t
+}
+
// Database wraps access to tries and contract code.
type Database interface {
+ // Type returns the trie type backing this database (MPT or UBT).
+ Type() DatabaseType
+
// Reader returns a state reader associated with the specified state root.
Reader(root common.Hash) (Reader, error)
@@ -55,7 +70,7 @@ type Database interface {
// Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error
// if the commit fails.
- Commit(update *stateUpdate) error
+ Commit(update *StateUpdate) error
}
// Trie is a Ethereum Merkle Patricia trie.
@@ -139,184 +154,27 @@ type Trie interface {
// with the node that proves the absence of the key.
Prove(key []byte, proofDb ethdb.KeyValueWriter) error
- // IsVerkle returns true if the trie is verkle-tree based
- IsVerkle() bool
-}
-
-// CachingDB is an implementation of Database interface. It leverages both trie and
-// state snapshot to provide functionalities for state access. It's meant to be a
-// long-live object and has a few caches inside for sharing between blocks.
-type CachingDB struct {
- triedb *triedb.Database
- codedb *CodeDB
- snap *snapshot.Tree
+ // IsUBT returns true if the trie is unified binary trie based.
+ IsUBT() bool
}
// NewDatabase creates a state database with the provided data sources.
-func NewDatabase(triedb *triedb.Database, codedb *CodeDB) *CachingDB {
- if codedb == nil {
- codedb = NewCodeDB(triedb.Disk())
- }
- return &CachingDB{
- triedb: triedb,
- codedb: codedb,
+//
+// Deprecated, please use NewMPTDatabase or NewUBTDatabase directly.
+func NewDatabase(tdb *triedb.Database, codedb *CodeDB) Database {
+ if tdb.IsUBT() {
+ return NewUBTDatabase(tdb, codedb)
}
+ return NewMPTDatabase(tdb, codedb)
}
// NewDatabaseForTesting is similar to NewDatabase, but it initializes the caching
// db by using an ephemeral memory db with default config for testing.
-func NewDatabaseForTesting() *CachingDB {
+func NewDatabaseForTesting() Database {
db := rawdb.NewMemoryDatabase()
return NewDatabase(triedb.NewDatabase(db, nil), NewCodeDB(db))
}
-// WithSnapshot configures the provided contract code cache. Note that this
-// registration must be performed before the cachingDB is used.
-func (db *CachingDB) WithSnapshot(snapshot *snapshot.Tree) *CachingDB {
- db.snap = snapshot
- return db
-}
-
-// StateReader returns a state reader associated with the specified state root.
-func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
- var readers []StateReader
-
- // Configure the state reader using the standalone snapshot in hash mode.
- // This reader offers improved performance but is optional and only
- // partially useful if the snapshot is not fully generated.
- if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil {
- snap := db.snap.Snapshot(stateRoot)
- if snap != nil {
- readers = append(readers, newFlatReader(snap))
- }
- }
- // Configure the state reader using the path database in path mode.
- // This reader offers improved performance but is optional and only
- // partially useful if the snapshot data in path database is not
- // fully generated.
- if db.TrieDB().Scheme() == rawdb.PathScheme {
- reader, err := db.triedb.StateReader(stateRoot)
- if err == nil {
- readers = append(readers, newFlatReader(reader))
- }
- }
- // Configure the trie reader, which is expected to be available as the
- // gatekeeper unless the state is corrupted.
- tr, err := newTrieReader(stateRoot, db.triedb)
- if err != nil {
- return nil, err
- }
- readers = append(readers, tr)
-
- return newMultiStateReader(readers...)
-}
-
-// Reader implements Database, returning a reader associated with the specified
-// state root.
-func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
- sr, err := db.StateReader(stateRoot)
- if err != nil {
- return nil, err
- }
- return newReader(db.codedb.Reader(), sr), nil
-}
-
-// ReadersWithCacheStats creates a pair of state readers that share the same
-// underlying state reader and internal state cache, while maintaining separate
-// statistics respectively.
-func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reader, error) {
- r, err := db.StateReader(stateRoot)
- if err != nil {
- return nil, nil, err
- }
- sr := newStateReaderWithCache(r)
- ra := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
- rb := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
- return ra, rb, nil
-}
-
-// OpenTrie opens the main account trie at a specific root hash.
-func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
- if db.triedb.IsVerkle() {
- ts := overlay.LoadTransitionState(db.TrieDB().Disk(), root, db.triedb.IsVerkle())
- if ts.InTransition() {
- panic("state tree transition isn't supported yet")
- }
- if ts.Transitioned() {
- // Use BinaryTrie instead of VerkleTrie when IsVerkle is set
- // (IsVerkle actually means Binary Trie mode in this codebase)
- return bintrie.NewBinaryTrie(root, db.triedb)
- }
- }
- tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb)
- if err != nil {
- return nil, err
- }
- return tr, nil
-}
-
-// OpenStorageTrie opens the storage trie of an account.
-func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
- if db.triedb.IsVerkle() {
- return self, nil
- }
- tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), db.triedb)
- if err != nil {
- return nil, err
- }
- return tr, nil
-}
-
-// TrieDB retrieves any intermediate trie-node caching layer.
-func (db *CachingDB) TrieDB() *triedb.Database {
- return db.triedb
-}
-
-// Snapshot returns the underlying state snapshot.
-func (db *CachingDB) Snapshot() *snapshot.Tree {
- return db.snap
-}
-
-// Commit flushes all pending writes and finalizes the state transition,
-// committing the changes to the underlying storage. It returns an error
-// if the commit fails.
-func (db *CachingDB) Commit(update *stateUpdate) error {
- // Short circuit if nothing to commit
- if update.empty() {
- return nil
- }
- // Commit dirty contract code if any exists
- if len(update.codes) > 0 {
- batch := db.codedb.NewBatchWithSize(len(update.codes))
- for _, code := range update.codes {
- batch.Put(code.hash, code.blob)
- }
- if err := batch.Commit(); err != nil {
- return err
- }
- }
- // If snapshotting is enabled, update the snapshot tree with this new version
- if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil {
- if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil {
- log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err)
- }
- // Keep 128 diff layers in the memory, persistent layer is 129th.
- // - head layer is paired with HEAD state
- // - head-1 layer is paired with HEAD-1 state
- // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state
- if err := db.snap.Cap(update.root, TriesInMemory); err != nil {
- log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err)
- }
- }
- return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet())
-}
-
-// Iteratee returns a state iteratee associated with the specified state root,
-// through which the account iterator and storage iterator can be created.
-func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) {
- return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap)
-}
-
// mustCopyTrie returns a deep-copied trie.
func mustCopyTrie(t Trie) Trie {
switch t := t.(type) {
@@ -324,6 +182,8 @@ func mustCopyTrie(t Trie) Trie {
return t.Copy()
case *transitiontrie.TransitionTrie:
return t.Copy()
+ case *bintrie.BinaryTrie:
+ return t.Copy()
default:
panic(fmt.Errorf("unknown trie type %T", t))
}
diff --git a/core/state/database_history.go b/core/state/database_history.go
index 0dbb8cc546..fbf4ab5f9c 100644
--- a/core/state/database_history.go
+++ b/core/state/database_history.go
@@ -223,6 +223,12 @@ type HistoricDB struct {
codedb *CodeDB
}
+// Type returns the trie type of the underlying database.
+func (db *HistoricDB) Type() DatabaseType {
+ // TODO(rjl493456442) support UBT in the future
+ return TypeMPT
+}
+
// NewHistoricDatabase creates a historic state database.
func NewHistoricDatabase(triedb *triedb.Database, codedb *CodeDB) *HistoricDB {
return &HistoricDB{
@@ -291,7 +297,7 @@ func (db *HistoricDB) TrieDB() *triedb.Database {
// Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error
// if the commit fails.
-func (db *HistoricDB) Commit(update *stateUpdate) error {
+func (db *HistoricDB) Commit(update *StateUpdate) error {
return errors.New("not implemented")
}
diff --git a/core/state/database_iterator.go b/core/state/database_iterator.go
index 8fad66a1e8..1ff164b002 100644
--- a/core/state/database_iterator.go
+++ b/core/state/database_iterator.go
@@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/triedb"
)
@@ -57,9 +58,9 @@ type AccountIterator interface {
// An error will be returned if the preimage is not available.
Address() (common.Address, error)
- // Account returns the RLP encoded account the iterator is currently at.
+ // Account returns the account the iterator is currently at.
// An error will be retained if the iterator becomes invalid.
- Account() []byte
+ Account() *types.StateAccount
}
// StorageIterator is an iterator to step over the specific storage in the
@@ -73,7 +74,7 @@ type StorageIterator interface {
// Slot returns the storage slot the iterator is currently at. An error will
// be retained if the iterator becomes invalid.
- Slot() []byte
+ Slot() common.Hash
}
// Iteratee wraps the NewIterator methods for traversing the accounts and
@@ -131,10 +132,7 @@ func (ai *flatAccountIterator) Next() bool {
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (ai *flatAccountIterator) Error() error {
- if ai.err != nil {
- return ai.err
- }
- return ai.it.Error()
+ return errors.Join(ai.err, ai.it.Error())
}
// Hash returns the hash of the account or storage slot the iterator is
@@ -165,8 +163,8 @@ func (ai *flatAccountIterator) Address() (common.Address, error) {
// Account returns the account data the iterator is currently at. The account
// data is encoded as slim format from the underlying iterator, the conversion
// is required.
-func (ai *flatAccountIterator) Account() []byte {
- data, err := types.FullAccountRLP(ai.it.Account())
+func (ai *flatAccountIterator) Account() *types.StateAccount {
+ data, err := types.FullAccount(ai.it.Account())
if err != nil {
ai.err = err
return nil
@@ -176,6 +174,7 @@ func (ai *flatAccountIterator) Account() []byte {
// flatStorageIterator is a wrapper around the underlying flat state iterator.
type flatStorageIterator struct {
+ err error
it snapshot.StorageIterator
preimage PreimageReader
}
@@ -190,13 +189,16 @@ func newFlatStorageIterator(it snapshot.StorageIterator, preimage PreimageReader
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
func (si *flatStorageIterator) Next() bool {
+ if si.err != nil {
+ return false
+ }
return si.it.Next()
}
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (si *flatStorageIterator) Error() error {
- return si.it.Error()
+ return errors.Join(si.err, si.it.Error())
}
// Hash returns the hash of the account or storage slot the iterator is
@@ -225,14 +227,24 @@ func (si *flatStorageIterator) Key() (common.Hash, error) {
}
// Slot returns the storage slot data the iterator is currently at.
-func (si *flatStorageIterator) Slot() []byte {
- return si.it.Slot()
+func (si *flatStorageIterator) Slot() common.Hash {
+ // Perform the rlp-decode as the slot value is RLP-encoded
+ blob := si.it.Slot()
+ _, content, _, err := rlp.Split(blob)
+ if err != nil {
+ si.err = err
+ return common.Hash{}
+ }
+ var value common.Hash
+ value.SetBytes(content)
+ return value
}
// merkleIterator implements the Iterator interface, providing functions to traverse
// the accounts or storages with the manner of Merkle-Patricia-Trie.
type merkleIterator struct {
tr Trie
+ err error
it *trie.Iterator
account bool
}
@@ -254,13 +266,16 @@ func newMerkleTrieIterator(tr Trie, start common.Hash, account bool) (*merkleIte
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
func (ti *merkleIterator) Next() bool {
+ if ti.err != nil {
+ return false
+ }
return ti.it.Next()
}
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (ti *merkleIterator) Error() error {
- return ti.it.Err
+ return errors.Join(ti.err, ti.it.Err)
}
// Hash returns the hash of the account or storage slot the iterator is
@@ -287,11 +302,16 @@ func (ti *merkleIterator) Address() (common.Address, error) {
}
// Account returns the account data the iterator is currently at.
-func (ti *merkleIterator) Account() []byte {
+func (ti *merkleIterator) Account() *types.StateAccount {
if !ti.account {
return nil
}
- return ti.it.Value
+ var account types.StateAccount
+ if err := rlp.DecodeBytes(ti.it.Value, &account); err != nil {
+ ti.err = err
+ return nil
+ }
+ return &account
}
// Key returns the raw storage slot key the iterator is currently at.
@@ -308,11 +328,19 @@ func (ti *merkleIterator) Key() (common.Hash, error) {
}
// Slot returns the storage slot the iterator is currently at.
-func (ti *merkleIterator) Slot() []byte {
+func (ti *merkleIterator) Slot() common.Hash {
if ti.account {
- return nil
+ return common.Hash{}
}
- return ti.it.Value
+ // Perform the rlp-decode as the slot value is RLP-encoded
+ _, content, _, err := rlp.Split(ti.it.Value)
+ if err != nil {
+ ti.err = err
+ return common.Hash{}
+ }
+ var value common.Hash
+ value.SetBytes(content)
+ return value
}
// stateIteratee implements Iteratee interface, providing the state traversal
@@ -430,6 +458,6 @@ func (e exhaustedIterator) Key() (common.Hash, error) {
return common.Hash{}, nil
}
-func (e exhaustedIterator) Slot() []byte {
- return nil
+func (e exhaustedIterator) Slot() common.Hash {
+ return common.Hash{}
}
diff --git a/core/state/database_iterator_test.go b/core/state/database_iterator_test.go
index 87819e5526..8313f86403 100644
--- a/core/state/database_iterator_test.go
+++ b/core/state/database_iterator_test.go
@@ -24,7 +24,6 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
- "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
@@ -45,7 +44,7 @@ func TestExhaustedIterator(t *testing.T) {
if key, err := it.Key(); key != (common.Hash{}) || err != nil {
t.Fatalf("Key() = %x, %v; want zero, nil", key, err)
}
- if slot := it.Slot(); slot != nil {
+ if slot := it.Slot(); slot != (common.Hash{}) {
t.Fatalf("Slot() = %x, want nil", slot)
}
it.Release()
@@ -95,20 +94,16 @@ func testAccountIterator(t *testing.T, scheme string) {
hashes = append(hashes, hash)
// Decode and verify account data.
- blob := acctIt.Account()
- if blob == nil {
+ got := acctIt.Account()
+ if got == nil {
t.Fatalf("(%s) nil account at %x", scheme, hash)
}
- var decoded types.StateAccount
- if err := rlp.DecodeBytes(blob, &decoded); err != nil {
- t.Fatalf("(%s) bad RLP at %x: %v", scheme, hash, err)
- }
acc := addrByHash[hash]
- if decoded.Nonce != acc.nonce {
- t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, decoded.Nonce, acc.nonce)
+ if got.Nonce != acc.nonce {
+ t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, got.Nonce, acc.nonce)
}
- if decoded.Balance.Cmp(acc.balance) != 0 {
- t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, decoded.Balance, acc.balance)
+ if got.Balance.Cmp(acc.balance) != 0 {
+ t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, got.Balance, acc.balance)
}
// Verify address preimage resolution.
addr, err := acctIt.Address()
@@ -183,7 +178,7 @@ func testStorageIterator(t *testing.T, scheme string) {
t.Fatalf("(%s) storage hashes not ascending for %x", scheme, acc.address)
}
prevHash = hash
- if storageIt.Slot() == nil {
+ if storageIt.Slot() == (common.Hash{}) {
t.Fatalf("(%s) nil slot at %x", scheme, hash)
}
// Check key preimage resolution on first slot.
diff --git a/core/state/database_mpt.go b/core/state/database_mpt.go
new file mode 100644
index 0000000000..42c5f2e5ef
--- /dev/null
+++ b/core/state/database_mpt.go
@@ -0,0 +1,187 @@
+// 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 (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/state/snapshot"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/triedb"
+)
+
+// MPTDatabase is an implementation of Database interface for Merkle Patricia Tries.
+// It leverages both trie and state snapshot to provide functionalities for state
+// access.
+type MPTDatabase struct {
+ triedb *triedb.Database
+ codedb *CodeDB
+ snap *snapshot.Tree
+}
+
+// Type returns Merkle, indicating this database is backed by a Merkle Patricia Trie.
+func (db *MPTDatabase) Type() DatabaseType { return TypeMPT }
+
+// NewMPTDatabase creates a state database with the Merkle Patricia Trie manner.
+func NewMPTDatabase(tdb *triedb.Database, codedb *CodeDB) *MPTDatabase {
+ if codedb == nil {
+ codedb = NewCodeDB(tdb.Disk())
+ }
+ return &MPTDatabase{
+ triedb: tdb,
+ codedb: codedb,
+ }
+}
+
+// WithSnapshot configures the provided state snapshot. Note that this
+// registration must be performed before the MPTDatabase is used.
+func (db *MPTDatabase) WithSnapshot(snapshot *snapshot.Tree) Database {
+ db.snap = snapshot
+ return db
+}
+
+// StateReader returns a state reader associated with the specified state root.
+func (db *MPTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
+ var readers []StateReader
+
+ // Configure the state reader using the standalone snapshot in hash mode.
+ // This reader offers improved performance but is optional and only
+ // partially useful if the snapshot is not fully generated.
+ if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil {
+ snap := db.snap.Snapshot(stateRoot)
+ if snap != nil {
+ readers = append(readers, newFlatReader(snap))
+ }
+ }
+ // Configure the state reader using the path database in path mode.
+ // This reader offers improved performance but is optional and only
+ // partially useful if the snapshot data in path database is not
+ // fully generated.
+ if db.TrieDB().Scheme() == rawdb.PathScheme {
+ reader, err := db.triedb.StateReader(stateRoot)
+ if err == nil {
+ readers = append(readers, newFlatReader(reader))
+ }
+ }
+ // Configure the trie reader, which is expected to be available as the
+ // gatekeeper unless the state is corrupted.
+ tr, err := newMPTTrieReader(stateRoot, db.triedb)
+ if err != nil {
+ return nil, err
+ }
+ readers = append(readers, tr)
+
+ return newMultiStateReader(readers...)
+}
+
+// Reader implements Database, returning a reader associated with the specified
+// state root.
+func (db *MPTDatabase) Reader(stateRoot common.Hash) (Reader, error) {
+ sr, err := db.StateReader(stateRoot)
+ if err != nil {
+ return nil, err
+ }
+ return newReader(db.codedb.Reader(), sr), nil
+}
+
+// ReadersWithCacheStats creates a pair of state readers that share the same
+// underlying state reader and internal state cache, while maintaining separate
+// statistics respectively.
+func (db *MPTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reader, error) {
+ r, err := db.StateReader(stateRoot)
+ if err != nil {
+ return nil, nil, err
+ }
+ sr := newStateReaderWithCache(r)
+ ra := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
+ rb := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
+ return ra, rb, nil
+}
+
+// OpenTrie opens the main account trie at a specific root hash.
+func (db *MPTDatabase) OpenTrie(root common.Hash) (Trie, error) {
+ tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb)
+ if err != nil {
+ return nil, err
+ }
+ return tr, nil
+}
+
+// OpenStorageTrie opens the storage trie of an account.
+func (db *MPTDatabase) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
+ tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), db.triedb)
+ if err != nil {
+ return nil, err
+ }
+ return tr, nil
+}
+
+// TrieDB retrieves any intermediate trie-node caching layer.
+func (db *MPTDatabase) TrieDB() *triedb.Database {
+ return db.triedb
+}
+
+// Commit flushes all pending writes and finalizes the state transition,
+// committing the changes to the underlying storage. It returns an error
+// if the commit fails.
+func (db *MPTDatabase) Commit(update *StateUpdate) error {
+ // Short circuit if nothing to commit
+ if update.Empty() {
+ return nil
+ }
+ // Commit dirty contract code if any exists
+ if len(update.Codes) > 0 {
+ batch := db.codedb.NewBatchWithSize(len(update.Codes))
+ for _, code := range update.Codes {
+ batch.Put(code.Hash, code.Blob)
+ }
+ if err := batch.Commit(); err != nil {
+ return err
+ }
+ }
+ // Encode the state mutations in the MPT format
+ accounts, accountOrigin, storages, storageOrigin := update.EncodeMPTState()
+
+ // If snapshotting is enabled, update the snapshot tree with this new version
+ if db.snap != nil && db.snap.Snapshot(update.OriginRoot) != nil {
+ if err := db.snap.Update(update.Root, update.OriginRoot, accounts, storages); err != nil {
+ log.Warn("Failed to update snapshot tree", "from", update.OriginRoot, "to", update.Root, "err", err)
+ }
+ // Keep 128 diff layers in the memory, persistent layer is 129th.
+ // - head layer is paired with HEAD state
+ // - head-1 layer is paired with HEAD-1 state
+ // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state
+ if err := db.snap.Cap(update.Root, TriesInMemory); err != nil {
+ log.Warn("Failed to cap snapshot tree", "root", update.Root, "layers", TriesInMemory, "err", err)
+ }
+ }
+ return db.triedb.Update(update.Root, update.OriginRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{
+ Accounts: accounts,
+ AccountsOrigin: accountOrigin,
+ Storages: storages,
+ StoragesOrigin: storageOrigin,
+ RawStorageKey: update.StorageKeyType == StorageKeyPlain,
+ })
+}
+
+// Iteratee returns a state iteratee associated with the specified state root,
+// through which the account iterator and storage iterator can be created.
+func (db *MPTDatabase) Iteratee(root common.Hash) (Iteratee, error) {
+ return newStateIteratee(true, root, db.triedb, db.snap)
+}
diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go
new file mode 100644
index 0000000000..16579f6d6a
--- /dev/null
+++ b/core/state/database_ubt.go
@@ -0,0 +1,147 @@
+// 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 (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/trie/bintrie"
+ "github.com/ethereum/go-ethereum/triedb"
+)
+
+// UBTDatabase is an implementation of Database interface for Unified Binary Trie.
+// It provides the same functionality as MPTDatabase but uses unified binary
+// trie for state hashing instead of Merkle Patricia Tries.
+type UBTDatabase struct {
+ triedb *triedb.Database
+ codedb *CodeDB
+}
+
+// Type returns Binary, indicating this database is backed by a Universal Binary Trie.
+func (db *UBTDatabase) Type() DatabaseType { return TypeUBT }
+
+// NewUBTDatabase creates a state database with the Unified binary trie manner.
+func NewUBTDatabase(triedb *triedb.Database, codedb *CodeDB) *UBTDatabase {
+ if codedb == nil {
+ codedb = NewCodeDB(triedb.Disk())
+ }
+ return &UBTDatabase{
+ triedb: triedb,
+ codedb: codedb,
+ }
+}
+
+// StateReader returns a state reader associated with the specified state root.
+func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
+ var readers []StateReader
+
+ // Configure the state reader using the path database in path mode.
+ // This reader offers improved performance but is optional and only
+ // partially useful if the snapshot data in path database is not
+ // fully generated.
+ if db.TrieDB().Scheme() == rawdb.PathScheme {
+ reader, err := db.triedb.StateReader(stateRoot)
+ if err == nil {
+ readers = append(readers, newFlatReader(reader))
+ }
+ }
+ // Configure the trie reader, which is expected to be available as the
+ // gatekeeper unless the state is corrupted.
+ tr, err := newUBTTrieReader(stateRoot, db.triedb)
+ if err != nil {
+ return nil, err
+ }
+ readers = append(readers, tr)
+
+ return newMultiStateReader(readers...)
+}
+
+// Reader implements Database, returning a reader associated with the specified
+// state root.
+func (db *UBTDatabase) Reader(stateRoot common.Hash) (Reader, error) {
+ sr, err := db.StateReader(stateRoot)
+ if err != nil {
+ return nil, err
+ }
+ return newReader(db.codedb.Reader(), sr), nil
+}
+
+// ReadersWithCacheStats creates a pair of state readers that share the same
+// underlying state reader and internal state cache, while maintaining separate
+// statistics respectively.
+func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reader, error) {
+ r, err := db.StateReader(stateRoot)
+ if err != nil {
+ return nil, nil, err
+ }
+ sr := newStateReaderWithCache(r)
+ ra := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
+ rb := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
+ return ra, rb, nil
+}
+
+// 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, db.triedb.BinTrieGroupDepth())
+}
+
+// OpenStorageTrie opens the storage trie of an account. In binary trie mode,
+// all state objects share one unified trie, so the main trie is returned.
+func (db *UBTDatabase) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
+ return self, nil
+}
+
+// TrieDB retrieves any intermediate trie-node caching layer.
+func (db *UBTDatabase) TrieDB() *triedb.Database {
+ return db.triedb
+}
+
+// Commit flushes all pending writes and finalizes the state transition,
+// committing the changes to the underlying storage. It returns an error
+// if the commit fails.
+func (db *UBTDatabase) Commit(update *StateUpdate) error {
+ // Short circuit if nothing to commit
+ if update.Empty() {
+ return nil
+ }
+ // Commit dirty contract code if any exists
+ if len(update.Codes) > 0 {
+ batch := db.codedb.NewBatchWithSize(len(update.Codes))
+ for _, code := range update.Codes {
+ batch.Put(code.Hash, code.Blob)
+ }
+ if err := batch.Commit(); err != nil {
+ return err
+ }
+ }
+ // Encode the state mutations in the UBT format
+ accounts, accountOrigin, storages, storageOrigin := update.EncodeUBTState()
+
+ return db.triedb.Update(update.Root, update.OriginRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{
+ Accounts: accounts,
+ AccountsOrigin: accountOrigin,
+ Storages: storages,
+ StoragesOrigin: storageOrigin,
+ RawStorageKey: update.StorageKeyType == StorageKeyPlain,
+ })
+}
+
+// Iteratee returns a state iteratee associated with the specified state root,
+// through which the account iterator and storage iterator can be created.
+func (db *UBTDatabase) Iteratee(root common.Hash) (Iteratee, error) {
+ return newStateIteratee(false, root, db.triedb, nil)
+}
diff --git a/core/state/dump.go b/core/state/dump.go
index 71138143d9..cbf53de053 100644
--- a/core/state/dump.go
+++ b/core/state/dump.go
@@ -24,9 +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/log"
- "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie/bintrie"
)
@@ -115,7 +113,7 @@ func (d iterativeDump) OnRoot(root common.Hash) {
// DumpToCollector iterates the state according to the given options and inserts
// the items into a collector for aggregation or serialization.
-func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) {
+func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte, err error) {
// Sanitize the input to allow nil configs
if conf == nil {
conf = new(DumpConfig)
@@ -131,7 +129,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil {
- return nil
+ return nil, err
}
var startHash common.Hash
if conf.Start != nil {
@@ -139,14 +137,17 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
}
acctIt, err := iteratee.NewAccountIterator(startHash)
if err != nil {
- return nil
+ return nil, err
}
defer acctIt.Release()
for acctIt.Next() {
- var data types.StateAccount
- if err := rlp.DecodeBytes(acctIt.Account(), &data); err != nil {
- panic(err)
+ data := acctIt.Account()
+ if err := acctIt.Error(); err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, fmt.Errorf("unexpected nil account value")
}
var (
account = DumpAccount{
@@ -168,7 +169,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
address = &addrBytes
account.Address = address
}
- obj := newObject(s, addrBytes, &data)
+ obj := newObject(s, addrBytes, data)
if !conf.SkipCode {
account.Code = obj.Code()
}
@@ -177,20 +178,19 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
storageIt, err := iteratee.NewStorageIterator(acctIt.Hash(), common.Hash{})
if err != nil {
- log.Error("Failed to load storage trie", "err", err)
- continue
+ return nil, err
}
for storageIt.Next() {
- _, content, _, err := rlp.Split(storageIt.Slot())
- if err != nil {
- log.Error("Failed to decode the value returned by iterator", "error", err)
- continue
+ val := storageIt.Slot()
+ if err := storageIt.Error(); err != nil {
+ storageIt.Release()
+ return nil, err
}
key, err := storageIt.Key()
if err != nil {
continue
}
- account.Storage[key] = common.Bytes2Hex(content)
+ account.Storage[key] = common.Bytes2Hex(common.TrimLeftZeroes(val[:]))
}
storageIt.Release()
}
@@ -211,7 +211,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages)
}
log.Info("Trie dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start)))
- return nextKey
+ return nextKey, nil
}
// DumpBinTrieLeaves collects all binary trie leaf nodes into the provided map.
@@ -242,7 +242,8 @@ func (s *StateDB) RawDump(opts *DumpConfig) Dump {
dump := &Dump{
Accounts: make(map[string]DumpAccount),
}
- dump.Next = s.DumpToCollector(dump, opts)
+ next, _ := s.DumpToCollector(dump, opts)
+ dump.Next = next
return *dump
}
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 fe0ec71f2d..be07cec0f9 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -148,70 +148,28 @@ func (r *flatReader) Storage(addr common.Address, key common.Hash) (common.Hash,
return value, nil
}
-// trieReader implements the StateReader interface, providing functions to access
-// state from the referenced trie.
+// mptTrieReader implements the StateReader interface, providing functions to
+// access state from the referenced Merkle-Patricia-tree.
//
-// trieReader is safe for concurrent read.
-type trieReader struct {
- root common.Hash // State root which uniquely represent a state
+// mptTrieReader is safe for concurrent read.
+type mptTrieReader struct {
+ root common.Hash // State root which uniquely represents a state
db *triedb.Database // Database for loading trie
- // Main trie, resolved in constructor. Note either the Merkle-Patricia-tree
- // or Verkle-tree is not safe for concurrent read.
- mainTrie Trie
-
+ mainTrie Trie // Main trie, resolved in constructor, not thread-safe
subRoots map[common.Address]common.Hash // Set of storage roots, cached when the account is resolved
subTries map[common.Address]Trie // Group of storage tries, cached when it's resolved
lock sync.Mutex // Lock for protecting concurrent read
}
-// newTrieReader constructs a trie reader of the specific state. An error will be
-// returned if the associated trie specified by root is not existent.
-func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) {
- var (
- tr Trie
- err error
- )
- if !db.IsVerkle() {
- tr, err = trie.NewStateTrie(trie.StateTrieID(root), db)
- } else {
- // When IsVerkle() is true, create a BinaryTrie wrapped in TransitionTrie
- binTrie, binErr := bintrie.NewBinaryTrie(root, db)
- if binErr != nil {
- return nil, binErr
- }
-
- // Based on the transition status, determine if the overlay
- // tree needs to be created, or if a single, target tree is
- // to be picked.
- ts := overlay.LoadTransitionState(db.Disk(), root, true)
- if ts.InTransition() {
- mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db)
- if err != nil {
- return nil, err
- }
- tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false)
- } else {
- // HACK: Use TransitionTrie with nil base as a wrapper to make BinaryTrie
- // satisfy the Trie interface. This works around the import cycle between
- // trie and trie/bintrie packages.
- //
- // TODO: In future PRs, refactor the package structure to avoid this hack:
- // - Option 1: Move common interfaces (Trie, NodeIterator) to a separate
- // package that both trie and trie/bintrie can import
- // - Option 2: Create a factory function in the trie package that returns
- // BinaryTrie as a Trie interface without direct import
- // - Option 3: Move BinaryTrie to the main trie package
- //
- // The current approach works but adds unnecessary overhead and complexity
- // by using TransitionTrie when there's no actual transition happening.
- tr = transitiontrie.NewTransitionTrie(nil, binTrie, false)
- }
- }
+// newMPTTrieReader constructs a Merkle-Patricia-tree reader of the specific state.
+// An error will be returned if the associated trie specified by root is not existent.
+func newMPTTrieReader(root common.Hash, db *triedb.Database) (*mptTrieReader, error) {
+ tr, err := trie.NewStateTrie(trie.StateTrieID(root), db)
if err != nil {
return nil, err
}
- return &trieReader{
+ return &mptTrieReader{
root: root,
db: db,
mainTrie: tr,
@@ -221,7 +179,7 @@ func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) {
}
// account is the inner version of Account and assumes the r.lock is already held.
-func (r *trieReader) account(addr common.Address) (*types.StateAccount, error) {
+func (r *mptTrieReader) account(addr common.Address) (*types.StateAccount, error) {
account, err := r.mainTrie.GetAccount(addr)
if err != nil {
return nil, err
@@ -236,9 +194,9 @@ func (r *trieReader) account(addr common.Address) (*types.StateAccount, error) {
// Account implements StateReader, retrieving the account specified by the address.
//
-// An error will be returned if the trie state is corrupted. An nil account
+// An error will be returned if the trie state is corrupted. A nil account
// will be returned if it's not existent in the trie.
-func (r *trieReader) Account(addr common.Address) (*types.StateAccount, error) {
+func (r *mptTrieReader) Account(addr common.Address) (*types.StateAccount, error) {
r.lock.Lock()
defer r.lock.Unlock()
@@ -250,43 +208,118 @@ func (r *trieReader) Account(addr common.Address) (*types.StateAccount, error) {
//
// An error will be returned if the trie state is corrupted. An empty storage
// slot will be returned if it's not existent in the trie.
-func (r *trieReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
+func (r *mptTrieReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
r.lock.Lock()
defer r.lock.Unlock()
- var (
- tr Trie
- found bool
- value common.Hash
- )
- if r.db.IsVerkle() {
- tr = r.mainTrie
- } else {
- tr, found = r.subTries[addr]
- if !found {
- root, ok := r.subRoots[addr]
+ tr, found := r.subTries[addr]
+ if !found {
+ root, ok := r.subRoots[addr]
- // The storage slot is accessed without account caching. It's unexpected
- // behavior but try to resolve the account first anyway.
- if !ok {
- _, err := r.account(addr)
- if err != nil {
- return common.Hash{}, err
- }
- root = r.subRoots[addr]
- }
- var err error
- tr, err = trie.NewStateTrie(trie.StorageTrieID(r.root, crypto.Keccak256Hash(addr.Bytes()), root), r.db)
+ // The storage slot is accessed without account caching. It's unexpected
+ // behavior but try to resolve the account first anyway.
+ if !ok {
+ _, err := r.account(addr)
if err != nil {
return common.Hash{}, err
}
- r.subTries[addr] = tr
+ root = r.subRoots[addr]
}
+ var err error
+ tr, err = trie.NewStateTrie(trie.StorageTrieID(r.root, crypto.Keccak256Hash(addr.Bytes()), root), r.db)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ r.subTries[addr] = tr
}
ret, err := tr.GetStorage(addr, key.Bytes())
if err != nil {
return common.Hash{}, err
}
+ var value common.Hash
+ value.SetBytes(ret)
+ return value, nil
+}
+
+// ubtTrieReader implements the StateReader interface, providing functions to access
+// state from the referenced Unified-binary-trie.
+//
+// ubtTrieReader is safe for concurrent read.
+type ubtTrieReader struct {
+ root common.Hash // State root which uniquely represents a state
+ db *triedb.Database // Database for loading trie
+ tr Trie // Referenced unified binary trie
+ lock sync.Mutex // Lock for protecting concurrent read
+}
+
+// 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, db.BinTrieGroupDepth())
+ if binErr != nil {
+ return nil, binErr
+ }
+ // Based on the transition status, determine if the overlay
+ // tree needs to be created, or if a single, target tree is
+ // to be picked.
+ var (
+ tr Trie
+ ts = overlay.LoadTransitionState(db.Disk(), root, true)
+ )
+ if ts.InTransition() {
+ mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db)
+ if err != nil {
+ return nil, err
+ }
+ tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false)
+ } else {
+ // HACK: Use TransitionTrie with nil base as a wrapper to make BinaryTrie
+ // satisfy the Trie interface. This works around the import cycle between
+ // trie and trie/bintrie packages.
+ //
+ // TODO: In future PRs, refactor the package structure to avoid this hack:
+ // - Option 1: Move common interfaces (Trie, NodeIterator) to a separate
+ // package that both trie and trie/bintrie can import
+ // - Option 2: Create a factory function in the trie package that returns
+ // BinaryTrie as a Trie interface without direct import
+ // - Option 3: Move BinaryTrie to the main trie package
+ //
+ // The current approach works but adds unnecessary overhead and complexity
+ // by using TransitionTrie when there's no actual transition happening.
+ tr = transitiontrie.NewTransitionTrie(nil, binTrie, false)
+ }
+ return &ubtTrieReader{
+ root: root,
+ db: db,
+ tr: tr,
+ }, nil
+}
+
+// Account implements StateReader, retrieving the account specified by the address.
+//
+// An error will be returned if the trie state is corrupted. A nil account
+// will be returned if it's not existent in the trie.
+func (r *ubtTrieReader) Account(addr common.Address) (*types.StateAccount, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ return r.tr.GetAccount(addr)
+}
+
+// Storage implements StateReader, retrieving the storage slot specified by the
+// address and slot key.
+//
+// An error will be returned if the trie state is corrupted. An empty storage
+// slot will be returned if it's not existent in the trie.
+func (r *ubtTrieReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ ret, err := r.tr.GetStorage(addr, key.Bytes())
+ if err != nil {
+ return common.Hash{}, err
+ }
+ var value common.Hash
value.SetBytes(ret)
return value, nil
}
diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go
new file mode 100644
index 0000000000..ff315ac5eb
--- /dev/null
+++ b/core/state/reader_eip_7928.go
@@ -0,0 +1,247 @@
+// 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 (
+ "sync"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/types/bal"
+)
+
+// The EIP27928 reader utilizes a hierarchical architecture to optimize state
+// access during block execution:
+//
+// - Base layer: The reader is initialized with the pre-transition state root,
+// providing the access of the state.
+//
+// - Prefetching Layer: This base reader is wrapped by newPrefetchStateReader.
+// Using an Access List hint, it asynchronously fetches required state data
+// in the background, minimizing I/O blocking during transaction processing.
+//
+// - Execution Layer: To support parallel transaction execution within the EIP
+// 7928 context, readers are wrapped in ReaderWithBlockLevelAccessList.
+// This layer provides a "unified view" by merging the pre-transition state
+// with mutated states from preceding transactions in the block.
+//
+// - Tracking Layer: Finally, the readerTracker wraps the execution reader to
+// capture all state reads made during a specific transaction. These individual
+// reads are subsequently merged to construct a comprehensive access list
+// for the entire block.
+//
+// The architecture can be illustrated by the diagram below:
+//
+// ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐
+// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │
+// │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │
+// └──────────────┬──────────────┘ └──────────────┬──────────────┘
+// │ │
+// └────────────────┬─────────────────┘
+// │
+// ┌──────────────┴──────────────┐
+// │ newPrefetchStateReader │ (Async I/O)
+// │ (Access List Hint driven) │
+// └──────────────┬──────────────┘
+// │
+// ┌──────────────┴──────────────┐
+// │ Base Reader │ (State Root)
+// │ (State & Contract Code) │
+// └─────────────────────────────┘
+
+// Note: The block producer, which is responsible for generating the block
+// along with the block-level access list, does not maintain the internal
+// hierarchy (e.g., PrefetchStateReader or ReaderWithBlockLevelAL).
+// Instead, it directly utilizes the readerTracker, wrapped around the
+// base reader, to construct the access list.
+
+type fetchTask struct {
+ addr common.Address
+ slots []common.Hash
+}
+
+func (t *fetchTask) weight() int { return 1 + len(t.slots) }
+
+type prefetchStateReader struct {
+ StateReader
+
+ tasks []*fetchTask
+ nThreads int
+ done chan struct{}
+ term chan struct{}
+ closeOnce sync.Once
+}
+
+// nolint:unused
+func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader {
+ tasks := make([]*fetchTask, 0, len(accessList))
+ for addr, slots := range accessList {
+ tasks = append(tasks, &fetchTask{
+ addr: addr,
+ slots: slots,
+ })
+ }
+ return newPrefetchStateReaderInternal(reader, tasks, nThreads)
+}
+
+func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThreads int) *prefetchStateReader {
+ r := &prefetchStateReader{
+ StateReader: reader,
+ tasks: tasks,
+ nThreads: nThreads,
+ done: make(chan struct{}),
+ term: make(chan struct{}),
+ }
+ go r.prefetch()
+ return r
+}
+
+func (r *prefetchStateReader) Close() {
+ r.closeOnce.Do(func() {
+ close(r.term)
+ <-r.done
+ })
+}
+
+func (r *prefetchStateReader) Wait() error {
+ select {
+ case <-r.term:
+ return nil
+ case <-r.done:
+ return nil
+ }
+}
+
+func (r *prefetchStateReader) prefetch() {
+ defer close(r.done)
+
+ if len(r.tasks) == 0 {
+ return
+ }
+ var total int
+ for _, t := range r.tasks {
+ total += t.weight()
+ }
+ var (
+ wg sync.WaitGroup
+ unit = (total + r.nThreads - 1) / r.nThreads // round-up the per worker unit
+ )
+ for i := 0; i < r.nThreads; i++ {
+ start := i * unit
+ if start >= total {
+ break
+ }
+ limit := (i + 1) * unit
+ if i == r.nThreads-1 {
+ limit = total
+ }
+ // Schedule the worker for prefetching, the items on the range [start, limit)
+ // is exclusively assigned for this worker.
+ wg.Add(1)
+ go func(workerID, startW, endW int) {
+ r.process(startW, endW)
+ wg.Done()
+ }(i, start, limit)
+ }
+ wg.Wait()
+}
+
+func (r *prefetchStateReader) process(start, limit int) {
+ var total = 0
+ for _, t := range r.tasks {
+ tw := t.weight()
+ if total+tw > start {
+ s := 0
+ if start > total {
+ s = start - total
+ }
+ l := tw
+ if limit < total+tw {
+ l = limit - total
+ }
+ for j := s; j < l; j++ {
+ select {
+ case <-r.term:
+ return
+ default:
+ if j == 0 {
+ r.StateReader.Account(t.addr)
+ } else {
+ r.StateReader.Storage(t.addr, t.slots[j-1])
+ }
+ }
+ }
+ }
+ total += tw
+ if total >= limit {
+ return
+ }
+ }
+}
+
+// ReaderWithBlockLevelAccessList provides state access that reflects the
+// pre-transition state combined with the mutations made by transactions
+// prior to TxIndex.
+type ReaderWithBlockLevelAccessList struct {
+ Reader
+ AccessList *bal.ConstructionBlockAccessList
+ TxIndex int
+}
+
+// NewReaderWithBlockLevelAccessList constructs a reader for accessing states
+// with the mutations made by transactions prior to txIndex.
+//
+// The txIndex refers to the call frame as such:
+// - 0 for pre‑execution system contract calls.
+// - 1 … n for transactions (in block order).
+// - n + 1 for post‑execution system contract calls.
+func NewReaderWithBlockLevelAccessList(base Reader, accessList *bal.ConstructionBlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList {
+ return &ReaderWithBlockLevelAccessList{
+ Reader: base,
+ AccessList: accessList,
+ TxIndex: txIndex,
+ }
+}
+
+// Account implements Reader, returning the account with the specific address.
+func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (*types.StateAccount, error) {
+ panic("implement me")
+}
+
+// Storage implements Reader, returning the storage slot with the specific
+// address and slot key.
+func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
+ panic("implement me")
+}
+
+// Has implements Reader, returning the flag indicating whether the contract
+// code with specified address and hash exists or not.
+func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash common.Hash) bool {
+ panic("implement me")
+}
+
+// Code implements Reader, returning the contract code with specified address
+// and hash.
+func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) ([]byte, error) {
+ panic("implement me")
+}
+
+// CodeSize implements Reader, returning the contract code size with specified
+// address and hash.
+func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
+ panic("implement me")
+}
diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go
new file mode 100644
index 0000000000..b2d432258c
--- /dev/null
+++ b/core/state/reader_eip_7928_test.go
@@ -0,0 +1,145 @@
+// 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 (
+ "fmt"
+ "math/rand"
+ "sync"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/internal/testrand"
+)
+
+type countingStateReader struct {
+ accounts map[common.Address]int
+ storages map[common.Address]map[common.Hash]int
+ lock sync.Mutex
+}
+
+func newRefStateReader() *countingStateReader {
+ return &countingStateReader{
+ accounts: make(map[common.Address]int),
+ storages: make(map[common.Address]map[common.Hash]int),
+ }
+}
+
+func (r *countingStateReader) validate(total int) error {
+ var sum int
+ for addr, n := range r.accounts {
+ if n != 1 {
+ return fmt.Errorf("duplicated account access: %x-%d", addr, n)
+ }
+ sum += 1
+
+ slots, exists := r.storages[addr]
+ if !exists {
+ continue
+ }
+ for key, n := range slots {
+ if n != 1 {
+ return fmt.Errorf("duplicated storage access: %x-%x-%d", addr, key, n)
+ }
+ sum += 1
+ }
+ }
+ for addr := range r.storages {
+ _, exists := r.accounts[addr]
+ if !exists {
+ return fmt.Errorf("dangling storage access: %x", addr)
+ }
+ }
+ if sum != total {
+ return fmt.Errorf("unexpected number of access, want: %d, got: %d", total, sum)
+ }
+ return nil
+}
+
+func (r *countingStateReader) Account(addr common.Address) (*types.StateAccount, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ r.accounts[addr] += 1
+ return nil, nil
+}
+func (r *countingStateReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ slots, exists := r.storages[addr]
+ if !exists {
+ slots = make(map[common.Hash]int)
+ r.storages[addr] = slots
+ }
+ slots[slot] += 1
+ return common.Hash{}, nil
+}
+
+func makeFetchTasks(n int) ([]*fetchTask, int) {
+ var (
+ total int
+ tasks []*fetchTask
+ )
+ for i := 0; i < n; i++ {
+ var slots []common.Hash
+ if rand.Intn(3) != 0 {
+ for j := 0; j < rand.Intn(100); j++ {
+ slots = append(slots, testrand.Hash())
+ }
+ }
+ tasks = append(tasks, &fetchTask{
+ addr: testrand.Address(),
+ slots: slots,
+ })
+ total += len(slots) + 1
+ }
+ return tasks, total
+}
+
+func TestPrefetchReader(t *testing.T) {
+ type suite struct {
+ tasks []*fetchTask
+ threads int
+ total int
+ }
+ var suites []suite
+ for i := 0; i < 100; i++ {
+ tasks, total := makeFetchTasks(100)
+ suites = append(suites, suite{
+ tasks: tasks,
+ threads: rand.Intn(30) + 1,
+ total: total,
+ })
+ }
+ // num(tasks) < num(threads)
+ tasks, total := makeFetchTasks(1)
+ suites = append(suites, suite{
+ tasks: tasks,
+ threads: 100,
+ total: total,
+ })
+ for _, s := range suites {
+ r := newRefStateReader()
+ pr := newPrefetchStateReaderInternal(r, s.tasks, s.threads)
+ pr.Wait()
+ if err := r.validate(s.total); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/core/state/snapshot/iterator_test.go b/core/state/snapshot/iterator_test.go
index dd6c4cf968..8e473aa312 100644
--- a/core/state/snapshot/iterator_test.go
+++ b/core/state/snapshot/iterator_test.go
@@ -342,7 +342,7 @@ func TestAccountIteratorTraversalValues(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 {
@@ -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 ec0c511737..ce456e7668 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
- "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
@@ -154,7 +153,7 @@ func (s *stateObject) getTrie() (Trie, error) {
func (s *stateObject) getPrefetchedTrie() Trie {
// If there's nothing to meaningfully return, let the user figure it out by
// pulling the trie from disk.
- if (s.data.Root == types.EmptyRootHash && !s.db.db.TrieDB().IsVerkle()) || s.db.prefetcher == nil {
+ if (s.data.Root == types.EmptyRootHash && s.db.db.Type().Is(TypeMPT)) || s.db.prefetcher == nil {
return nil
}
// Attempt to retrieve the trie from the prefetcher
@@ -163,8 +162,11 @@ func (s *stateObject) getPrefetchedTrie() Trie {
// GetState retrieves a value associated with the given storage key.
func (s *stateObject) GetState(key common.Hash) common.Hash {
- value, _ := s.getState(key)
- return value
+ value, dirty := s.dirtyStorage[key]
+ if dirty {
+ return value
+ }
+ return s.GetCommittedState(key)
}
// getState retrieves a value associated with the given storage key, along with
@@ -181,6 +183,10 @@ func (s *stateObject) getState(key common.Hash) (common.Hash, common.Hash) {
// GetCommittedState retrieves the value associated with the specific key
// 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.
+ 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
@@ -195,19 +201,6 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
// have been handles via pendingStorage above.
// 2) we don't have new values, and can deliver empty response back
if _, destructed := s.db.stateObjectsDestruct[s.address]; destructed {
- // Invoke the reader regardless and discard the returned value.
- // The returned value may not be empty, as it could belong to a
- // self-destructed contract.
- //
- // The read operation is still essential for correctly building
- // the block-level access list.
- //
- // TODO(rjl493456442) the reader interface can be extended with
- // Touch, recording the read access without the actual disk load.
- _, err := s.db.reader.Storage(s.address, key)
- if err != nil {
- s.db.setError(err)
- }
s.originStorage[key] = common.Hash{} // track the empty slot as origin value
return common.Hash{}
}
@@ -282,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 {
@@ -398,17 +398,8 @@ func (s *stateObject) updateRoot() {
}
// commitStorage overwrites the clean storage with the storage changes and
-// fulfills the storage diffs into the given accountUpdate struct.
-func (s *stateObject) commitStorage(op *accountUpdate) {
- var (
- encode = func(val common.Hash) []byte {
- if val == (common.Hash{}) {
- return nil
- }
- blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:]))
- return blob
- }
- )
+// fulfills the storage diffs into the given AccountUpdate struct.
+func (s *stateObject) commitStorage(op *AccountUpdate) {
for key, val := range s.pendingStorage {
// Skip the noop storage changes, it might be possible the value
// of tracked slot is same in originStorage and pendingStorage
@@ -418,20 +409,20 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
continue
}
hash := crypto.Keccak256Hash(key[:])
- if op.storages == nil {
- op.storages = make(map[common.Hash][]byte)
+ if op.Storages == nil {
+ op.Storages = make(map[common.Hash]common.Hash)
}
- op.storages[hash] = encode(val)
+ op.Storages[hash] = val
- if op.storagesOriginByKey == nil {
- op.storagesOriginByKey = make(map[common.Hash][]byte)
+ if op.StoragesOriginByKey == nil {
+ op.StoragesOriginByKey = make(map[common.Hash]common.Hash)
}
- if op.storagesOriginByHash == nil {
- op.storagesOriginByHash = make(map[common.Hash][]byte)
+ if op.StoragesOriginByHash == nil {
+ op.StoragesOriginByHash = make(map[common.Hash]common.Hash)
}
- origin := encode(s.originStorage[key])
- op.storagesOriginByKey[key] = origin
- op.storagesOriginByHash[hash] = origin
+ origin := s.originStorage[key]
+ op.StoragesOriginByKey[key] = origin
+ op.StoragesOriginByHash[hash] = origin
// Overwrite the clean value of storage slots
s.originStorage[key] = val
@@ -444,32 +435,32 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
//
// Note, commit may run concurrently across all the state objects. Do not assume
// thread-safe access to the statedb.
-func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
- // commit the account metadata changes
- op := &accountUpdate{
- address: s.address,
- data: types.SlimAccountRLP(s.data),
- }
- if s.origin != nil {
- op.origin = types.SlimAccountRLP(*s.origin)
+func (s *stateObject) commit() (*AccountUpdate, *trienode.NodeSet, error) {
+ // commit the account metadata changes, the data must be deep-copied
+ // to prevent accidental mutations later on (in practice the stateDB
+ // won't be modified after commit). The origin is safe to use directly.
+ op := &AccountUpdate{
+ Address: s.address,
+ Data: s.data.Copy(),
+ Origin: s.origin,
}
// commit the contract code if it's modified
if s.dirtyCode {
- op.code = &contractCode{
- hash: common.BytesToHash(s.CodeHash()),
- blob: s.code,
+ op.Code = &ContractCode{
+ Hash: common.BytesToHash(s.CodeHash()),
+ Blob: s.code,
}
s.dirtyCode = false // reset the dirty flag
if s.origin == nil {
- op.code.originHash = types.EmptyCodeHash
+ op.Code.OriginHash = types.EmptyCodeHash
} else {
- op.code.originHash = common.BytesToHash(s.origin.CodeHash)
+ op.Code.OriginHash = common.BytesToHash(s.origin.CodeHash)
}
}
// Commit storage changes and the associated storage trie
s.commitStorage(op)
- if len(op.storages) == 0 {
+ if len(op.Storages) == 0 {
// nothing changed, don't bother to commit the trie
s.origin = s.data.Copy()
return op, nil, nil
@@ -478,12 +469,13 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
// The main account trie commit in stateDB.commit() already calls
// CollectNodes on this trie, so calling Commit here again would
// redundantly traverse and serialize the entire tree per dirty account.
- if s.db.GetTrie().IsVerkle() {
+ if s.db.GetTrie().IsUBT() {
s.origin = s.data.Copy()
return op, nil, nil
}
- root, nodes := s.trie.Commit(false)
- s.data.Root = root
+ // The storage trie root is omitted, as it has already been updated in the
+ // previous updateRoot step.
+ _, nodes := s.trie.Commit(false)
s.origin = s.data.Copy()
return op, nodes, nil
}
diff --git a/core/state/state_sizer.go b/core/state/state_sizer.go
index 02b73e5575..3293d7e950 100644
--- a/core/state/state_sizer.go
+++ b/core/state/state_sizer.go
@@ -125,16 +125,17 @@ func (s SizeStats) add(diff SizeStats) SizeStats {
}
// calSizeStats measures the state size changes of the provided state update.
-func calSizeStats(update *stateUpdate) (SizeStats, error) {
+func calSizeStats(update *StateUpdate) (SizeStats, error) {
stats := SizeStats{
- BlockNumber: update.blockNumber,
- StateRoot: update.root,
+ BlockNumber: update.BlockNumber,
+ StateRoot: update.Root,
}
+ accounts, accountOrigin, storages, storageOrigin := update.EncodeMPTState()
// Measure the account changes
- for addr, oldValue := range update.accountsOrigin {
+ for addr, oldValue := range accountOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes())
- newValue, exists := update.accounts[addrHash]
+ newValue, exists := accounts[addrHash]
if !exists {
return SizeStats{}, fmt.Errorf("account %x not found", addr)
}
@@ -156,9 +157,9 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
}
// Measure storage changes
- for addr, slots := range update.storagesOrigin {
+ for addr, slots := range storageOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes())
- subset, exists := update.storages[addrHash]
+ subset, exists := storages[addrHash]
if !exists {
return SizeStats{}, fmt.Errorf("storage %x not found", addr)
}
@@ -167,7 +168,7 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
exists bool
newValue []byte
)
- if update.rawStorageKey {
+ if update.StorageKeyType == StorageKeyPlain {
newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())]
} else {
newValue, exists = subset[key]
@@ -194,7 +195,7 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
}
// Measure trienode changes
- for owner, subset := range update.nodes.Sets {
+ for owner, subset := range update.Nodes.Sets {
var (
keyPrefix int64
isAccount = owner == (common.Hash{})
@@ -244,13 +245,13 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
}
codeExists := make(map[common.Hash]struct{})
- for _, code := range update.codes {
- if _, ok := codeExists[code.hash]; ok || code.duplicate {
+ for _, code := range update.Codes {
+ if _, ok := codeExists[code.Hash]; ok || code.Duplicate {
continue
}
stats.ContractCodes += 1
- stats.ContractCodeBytes += codeKeySize + int64(len(code.blob))
- codeExists[code.hash] = struct{}{}
+ stats.ContractCodeBytes += codeKeySize + int64(len(code.Blob))
+ codeExists[code.Hash] = struct{}{}
}
return stats, nil
}
@@ -267,7 +268,7 @@ type SizeTracker struct {
triedb *triedb.Database
abort chan struct{}
aborted chan struct{}
- updateCh chan *stateUpdate
+ updateCh chan *StateUpdate
queryCh chan *stateSizeQuery
}
@@ -281,7 +282,7 @@ func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTrack
triedb: triedb,
abort: make(chan struct{}),
aborted: make(chan struct{}),
- updateCh: make(chan *stateUpdate),
+ updateCh: make(chan *StateUpdate),
queryCh: make(chan *stateSizeQuery),
}
go t.run()
@@ -328,9 +329,9 @@ func (t *SizeTracker) run() {
for {
select {
case u := <-t.updateCh:
- base, found := stats[u.originRoot]
+ base, found := stats[u.OriginRoot]
if !found {
- log.Debug("Ignored the state size without parent", "parent", u.originRoot, "root", u.root, "number", u.blockNumber)
+ log.Debug("Ignored the state size without parent", "parent", u.OriginRoot, "root", u.Root, "number", u.BlockNumber)
continue
}
diff, err := calSizeStats(u)
@@ -338,15 +339,15 @@ func (t *SizeTracker) run() {
continue
}
stat := base.add(diff)
- stats[u.root] = stat
- last = u.root
+ stats[u.Root] = stat
+ last = u.Root
// Publish statistics to metric system
stat.publish()
// Evict the stale statistics
- heap.Push(&h, stats[u.root])
- for len(h) > 0 && u.blockNumber-h[0].BlockNumber > statEvictThreshold {
+ heap.Push(&h, stats[u.Root])
+ for len(h) > 0 && u.BlockNumber-h[0].BlockNumber > statEvictThreshold {
delete(stats, h[0].StateRoot)
heap.Pop(&h)
}
@@ -402,7 +403,7 @@ wait:
}
var (
- updates = make(map[common.Hash]*stateUpdate)
+ updates = make(map[common.Hash]*StateUpdate)
children = make(map[common.Hash][]common.Hash)
done chan buildResult
)
@@ -410,9 +411,9 @@ wait:
for {
select {
case u := <-t.updateCh:
- updates[u.root] = u
- children[u.originRoot] = append(children[u.originRoot], u.root)
- log.Debug("Received state update", "root", u.root, "blockNumber", u.blockNumber)
+ updates[u.Root] = u
+ children[u.OriginRoot] = append(children[u.OriginRoot], u.Root)
+ log.Debug("Received state update", "root", u.Root, "blockNumber", u.BlockNumber)
case r := <-t.queryCh:
r.err = errors.New("state size is not initialized yet")
@@ -432,8 +433,8 @@ wait:
continue
}
done = make(chan buildResult)
- go t.build(entry.root, entry.blockNumber, done)
- log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.blockNumber)
+ go t.build(entry.Root, entry.BlockNumber, done)
+ log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.BlockNumber)
case result := <-done:
if result.err != nil {
@@ -646,8 +647,8 @@ func (t *SizeTracker) iterateTableParallel(closed chan struct{}, prefix []byte,
// Notify is an async method used to send the state update to the size tracker.
// It ignores empty updates (where no state changes occurred).
// If the channel is full, it drops the update to avoid blocking.
-func (t *SizeTracker) Notify(update *stateUpdate) {
- if update == nil || update.empty() {
+func (t *SizeTracker) Notify(update *StateUpdate) {
+ if update == nil || update.Empty() {
return
}
select {
diff --git a/core/state/state_sizer_test.go b/core/state/state_sizer_test.go
index b3203afd74..539f160985 100644
--- a/core/state/state_sizer_test.go
+++ b/core/state/state_sizer_test.go
@@ -160,7 +160,7 @@ func TestSizeTracker(t *testing.T) {
}
tracker.Notify(ret)
- if err := tdb.Commit(ret.root, false); err != nil {
+ if err := tdb.Commit(ret.Root, false); err != nil {
t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
}
@@ -169,7 +169,7 @@ func TestSizeTracker(t *testing.T) {
t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
}
trackedUpdates = append(trackedUpdates, diff)
- currentRoot = ret.root
+ currentRoot = ret.Root
}
finalRoot := rawdb.ReadSnapshotRoot(db)
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 8b09ea89f6..1c49d46020 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -18,6 +18,7 @@
package state
import (
+ "bytes"
"errors"
"fmt"
"maps"
@@ -31,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/core/stateless"
"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/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
@@ -126,6 +128,12 @@ type StateDB struct {
accessList *accessList
accessEvents *AccessEvents
+ // Per-transaction state access footprint for EIP-7928
+ 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
@@ -190,7 +198,7 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
accessList: newAccessList(),
transientStorage: newTransientStorage(),
}
- if db.TrieDB().IsVerkle() {
+ if db.Type().Is(TypeUBT) {
sdb.accessEvents = NewAccessEvents()
}
return sdb, nil
@@ -317,6 +325,11 @@ func (s *StateDB) Empty(addr common.Address) bool {
return so == nil || so.empty()
}
+// Touch accesses the specific account without returning anything.
+func (s *StateDB) Touch(addr common.Address) {
+ s.getStateObject(addr)
+}
+
// GetBalance retrieves the balance from the given address or 0 if object not found
func (s *StateDB) GetBalance(addr common.Address) *uint256.Int {
stateObject := s.getStateObject(addr)
@@ -338,6 +351,9 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 {
// GetStorageRoot retrieves the storage root from the given address or empty
// if object not found.
+//
+// Note: the storage root returned corresponds to the trie since last Intermediate
+// operation, some recent in-memory changes are excluded.
func (s *StateDB) GetStorageRoot(addr common.Address) common.Hash {
stateObject := s.getStateObject(addr)
if stateObject != nil {
@@ -576,6 +592,10 @@ func (s *StateDB) deleteStateObject(addr common.Address) {
// getStateObject retrieves a state object given by the address, returning nil if
// 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.
+ if s.stateAccessList != nil {
+ s.stateAccessList.AccountRead(addr)
+ }
// Prefer live objects if any is available
if obj := s.stateObjects[addr]; obj != nil {
return obj
@@ -678,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),
@@ -722,6 +743,9 @@ func (s *StateDB) Copy() *StateDB {
}
state.logs[hash] = cpy
}
+ if s.stateAccessList != nil {
+ state.stateAccessList = s.stateAccessList.Copy()
+ }
return state
}
@@ -757,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,
@@ -781,29 +805,69 @@ 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) {
- 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()) {
delete(s.stateObjects, obj.address)
s.markDelete(addr)
+
// We need to maintain account deletions explicitly (will remain
// set indefinitely). Note only the first occurred self-destruct
// event is tracked.
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)
}
@@ -819,6 +883,8 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
}
// Invalidate journal because reverting across transactions is not allowed.
s.clearJournalAndRefund()
+
+ return s.stateAccessList
}
// IntermediateRoot computes the current root hash of the state trie.
@@ -858,7 +924,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
start = time.Now()
workers errgroup.Group
)
- if s.db.TrieDB().IsVerkle() {
+ if s.db.Type().Is(TypeUBT) {
// Bypass per-account updateTrie() for binary trie. In binary trie mode
// there is only one unified trie (OpenStorageTrie returns self), so the
// per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie,
@@ -922,9 +988,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
}
}
// If witness building is enabled, gather all the read-only accesses.
- // Skip witness collection in Verkle mode, they will be gathered
- // together at the end.
- if s.witness != nil && !s.db.TrieDB().IsVerkle() {
+ // Skip witness collection in Unified-binary-trie mode, they will be
+ // gathered together at the end.
+ if s.witness != nil && s.db.Type().Is(TypeMPT) {
// Pull in anything that has been accessed before destruction
for _, obj := range s.stateObjectsDestruct {
// Skip any objects that haven't touched their storage
@@ -965,7 +1031,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// only a single trie is used for state hashing. Replacing a non-nil verkle tree
// here could result in losing uncommitted changes from storage.
start = time.Now()
- if s.prefetcher != nil && !s.db.TrieDB().IsVerkle() {
+ if s.prefetcher != nil && s.db.Type().Is(TypeMPT) {
if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil {
log.Error("Failed to retrieve account pre-fetcher trie")
} else {
@@ -1031,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() {
@@ -1042,11 +1109,11 @@ func (s *StateDB) clearJournalAndRefund() {
}
// deleteStorage is designed to delete the storage trie of a designated account.
-func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
+func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
var (
- nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
- storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil)
- storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot
+ nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
+ storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil)
+ storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot
)
iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil {
@@ -1062,19 +1129,24 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
nodes.AddNode(path, trienode.NewDeletedWithPrev(blob))
})
for it.Next() {
- slot := common.CopyBytes(it.Slot())
- if err := it.Error(); err != nil { // error might occur after Slot function
+ slot := it.Slot()
+ // Error might occur after Slot function
+ if err := it.Error(); err != nil {
return nil, nil, nil, err
}
+ if slot == (common.Hash{}) {
+ return nil, nil, nil, fmt.Errorf("unexpected empty storage slot, addr: %x, slot: %x", addrHash, it.Hash())
+ }
key := it.Hash()
- storages[key] = nil
+ storages[key] = common.Hash{}
storageOrigins[key] = slot
- if err := stack.Update(key.Bytes(), slot); err != nil {
+ if err := stack.Update(key.Bytes(), encodeSlot(slot)); err != nil {
return nil, nil, nil, err
}
}
- if err := it.Error(); err != nil { // error might occur during iteration
+ // Error might occur during iteration
+ if err := it.Error(); err != nil {
return nil, nil, nil, err
}
if stack.Hash() != root {
@@ -1101,10 +1173,10 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
// with their values be tracked as original value.
// In case (d), **original** account along with its storages should be deleted,
// with their values be tracked as original value.
-func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, []*trienode.NodeSet, error) {
+func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*AccountDelete, []*trienode.NodeSet, error) {
var (
nodes []*trienode.NodeSet
- deletes = make(map[common.Hash]*accountDelete)
+ deletes = make(map[common.Hash]*AccountDelete)
)
for addr, prevObj := range s.stateObjectsDestruct {
prev := prevObj.origin
@@ -1118,15 +1190,15 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
continue
}
// The account was existent, it can be either case (c) or (d).
- addrHash := crypto.Keccak256Hash(addr.Bytes())
- op := &accountDelete{
- address: addr,
- origin: types.SlimAccountRLP(*prev),
+ addrHash := prevObj.addrHash()
+ op := &AccountDelete{
+ Address: addr,
+ Origin: prev,
}
deletes[addrHash] = op
// Short circuit if the origin storage was empty.
- if prev.Root == types.EmptyRootHash || s.db.TrieDB().IsVerkle() {
+ if prev.Root == types.EmptyRootHash || s.db.Type().Is(TypeUBT) {
continue
}
if noStorageWiping {
@@ -1137,8 +1209,8 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
if err != nil {
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
}
- op.storages = storages
- op.storagesOrigin = storagesOrigin
+ op.Storages = storages
+ op.StoragesOrigin = storagesOrigin
// Aggregate the associated trie node changes.
nodes = append(nodes, set)
@@ -1153,13 +1225,13 @@ func (s *StateDB) GetTrie() Trie {
// commit gathers the state mutations accumulated along with the associated
// trie changes, resetting all internal flags with the new state as the base.
-func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) {
+func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*StateUpdate, error) {
// Short circuit in case any database failure occurred earlier.
if s.dbErr != nil {
return nil, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr)
}
// Finalize any pending changes and merge everything into the tries
- s.IntermediateRoot(deleteEmptyObjects)
+ root := s.IntermediateRoot(deleteEmptyObjects)
// Short circuit if any error occurs within the IntermediateRoot.
if s.dbErr != nil {
@@ -1174,7 +1246,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
lock sync.Mutex // protect two maps below
nodes = trienode.NewMergedNodeSet() // aggregated trie nodes
- updates = make(map[common.Hash]*accountUpdate, len(s.mutations)) // aggregated account updates
+ updates = make(map[common.Hash]*AccountUpdate, len(s.mutations)) // aggregated account updates
// merge aggregates the dirty trie nodes into the global set.
//
@@ -1221,7 +1293,6 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
// writes to run in parallel with the computations.
var (
start = time.Now()
- root common.Hash
workers errgroup.Group
)
// Schedule the account trie first since that will be the biggest, so give
@@ -1235,9 +1306,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
// code didn't anticipate for.
workers.Go(func() error {
// Write the account trie changes, measuring the amount of wasted time
- newroot, set := s.trie.Commit(true)
- root = newroot
-
+ _, set := s.trie.Commit(true)
if err := merge(set); err != nil {
return err
}
@@ -1305,12 +1374,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
origin := s.originalRoot
s.originalRoot = root
- return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes), nil
+ typ := StorageKeyHashed
+ if noStorageWiping {
+ typ = StorageKeyPlain
+ }
+ return NewStateUpdate(typ, origin, root, blockNumber, deletes, updates, nodes), nil
}
// commitAndFlush is a wrapper of commit which also commits the state mutations
// to the configured data stores.
-func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, deriveCodeFields bool) (*stateUpdate, error) {
+func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, deriveCodeFields bool) (*StateUpdate, error) {
ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
if err != nil {
return nil, err
@@ -1328,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
}
@@ -1351,17 +1424,17 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping
if err != nil {
return common.Hash{}, err
}
- return ret.root, nil
+ return ret.Root, nil
}
// CommitWithUpdate writes the state mutations and returns the state update for
// external processing (e.g., live tracing hooks or size tracker).
-func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *stateUpdate, error) {
+func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) {
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true)
if err != nil {
return common.Hash{}, nil, err
}
- return ret.root, ret, nil
+ return ret.Root, ret, nil
}
// Prepare handles the preparatory steps for executing a state transition with.
@@ -1406,6 +1479,10 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d
}
// Reset transient storage at the beginning of transaction execution
s.transientStorage = newTransientStorage()
+
+ if rules.IsAmsterdam {
+ s.stateAccessList = bal.NewConstructionBlockAccessList()
+ }
}
// AddAddressToAccessList adds the given address to the access list
diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go
index 3582185344..c796b416a3 100644
--- a/core/state/statedb_fuzz_test.go
+++ b/core/state/statedb_fuzz_test.go
@@ -182,11 +182,12 @@ func (test *stateTest) run() bool {
accountOrigin []map[common.Address][]byte
storages []map[common.Hash]map[common.Hash][]byte
storageOrigin []map[common.Address]map[common.Hash][]byte
- copyUpdate = func(update *stateUpdate) {
- accounts = append(accounts, maps.Clone(update.accounts))
- accountOrigin = append(accountOrigin, maps.Clone(update.accountsOrigin))
- storages = append(storages, maps.Clone(update.storages))
- storageOrigin = append(storageOrigin, maps.Clone(update.storagesOrigin))
+ copyUpdate = func(update *StateUpdate) {
+ accts, acctOrigin, slots, slotOrigin := update.EncodeMPTState()
+ accounts = append(accounts, maps.Clone(accts))
+ accountOrigin = append(accountOrigin, maps.Clone(acctOrigin))
+ storages = append(storages, maps.Clone(slots))
+ storageOrigin = append(storageOrigin, maps.Clone(slotOrigin))
}
disk = rawdb.NewMemoryDatabase()
tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults})
@@ -209,7 +210,7 @@ func (test *stateTest) run() bool {
if i != 0 {
root = roots[len(roots)-1]
}
- state, err := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps))
+ state, err := New(root, NewMPTDatabase(tdb, nil).WithSnapshot(snaps))
if err != nil {
panic(err)
}
@@ -232,11 +233,11 @@ func (test *stateTest) run() bool {
if err != nil {
panic(err)
}
- if ret.empty() {
+ if ret.Empty() {
return true
}
copyUpdate(ret)
- roots = append(roots, ret.root)
+ roots = append(roots, ret.Root)
}
for i := 0; i < len(test.actions); i++ {
root := types.EmptyRootHash
diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go
index 52cf98d19b..98d01343a4 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/stateless"
"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/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
@@ -98,10 +99,6 @@ func (s *hookedStateDB) GetState(addr common.Address, hash common.Hash) common.H
return s.inner.GetState(addr, hash)
}
-func (s *hookedStateDB) GetStorageRoot(addr common.Address) common.Hash {
- return s.inner.GetStorageRoot(addr)
-}
-
func (s *hookedStateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash {
return s.inner.GetTransientState(addr, key)
}
@@ -118,6 +115,10 @@ func (s *hookedStateDB) Exist(addr common.Address) bool {
return s.inner.Exist(addr)
}
+func (s *hookedStateDB) Touch(addr common.Address) {
+ s.inner.Touch(addr)
+}
+
func (s *hookedStateDB) Empty(addr common.Address) bool {
return s.inner.Empty(addr)
}
@@ -233,18 +234,17 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log {
return s.inner.LogsForBurnAccounts()
}
-func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
+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.
- s.inner.Finalise(deleteEmptyObjects)
- return
+ return s.inner.Finalise(deleteEmptyObjects)
}
// Collect all self-destructed addresses first, then sort them to ensure
// 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.
@@ -286,6 +286,9 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
s.hooks.OnCodeChange(addr, prevCodeHash, s.inner.GetCode(addr), types.EmptyCodeHash, nil)
}
}
-
- s.inner.Finalise(deleteEmptyObjects)
+ 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 d29b262eea..0bf9b50e7b 100644
--- a/core/state/statedb_test.go
+++ b/core/state/statedb_test.go
@@ -247,16 +247,16 @@ func TestCopyWithDirtyJournal(t *testing.T) {
orig.Finalise(true)
for i := byte(0); i < 255; i++ {
- root := orig.GetStorageRoot(common.BytesToAddress([]byte{i}))
- if root != (common.Hash{}) {
- t.Errorf("Unexpected storage root %x", root)
+ balance := orig.GetBalance(common.BytesToAddress([]byte{i}))
+ if !balance.IsZero() {
+ t.Errorf("Unexpected balance %x", root)
}
}
cpy.Finalise(true)
for i := byte(0); i < 255; i++ {
- root := cpy.GetStorageRoot(common.BytesToAddress([]byte{i}))
- if root != (common.Hash{}) {
- t.Errorf("Unexpected storage root %x", root)
+ balance := cpy.GetBalance(common.BytesToAddress([]byte{i}))
+ if !balance.IsZero() {
+ t.Errorf("Unexpected balance %x", root)
}
}
if cpy.IntermediateRoot(true) != orig.IntermediateRoot(true) {
@@ -394,9 +394,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
}
contractHash := s.GetCodeHash(addr)
emptyCode := contractHash == (common.Hash{}) || contractHash == types.EmptyCodeHash
- storageRoot := s.GetStorageRoot(addr)
- emptyStorage := storageRoot == (common.Hash{}) || storageRoot == types.EmptyRootHash
- if s.GetNonce(addr) == 0 && emptyCode && emptyStorage {
+ if s.GetNonce(addr) == 0 && emptyCode {
s.CreateContract(addr)
// We also set some code here, to prevent the
// CreateContract action from being performed twice in a row,
@@ -641,7 +639,7 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
{
have := state.transientStorage
want := checkstate.transientStorage
- if !maps.EqualFunc(have, want, maps.Equal) {
+ if !maps.Equal(have, want) {
return fmt.Errorf("transient storage differs ,have\n%v\nwant\n%v",
have.PrettyPrint(),
want.PrettyPrint())
@@ -664,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{})
@@ -693,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")
}
}
@@ -1276,7 +1320,7 @@ func TestDeleteStorage(t *testing.T) {
disk = rawdb.NewMemoryDatabase()
tdb = triedb.NewDatabase(disk, nil)
snaps, _ = snapshot.New(snapshot.Config{CacheSize: 10}, disk, tdb, types.EmptyRootHash)
- db = NewDatabase(tdb, nil).WithSnapshot(snaps)
+ db = NewMPTDatabase(tdb, nil).WithSnapshot(snaps)
state, _ = New(types.EmptyRootHash, db)
addr = common.HexToAddress("0x1")
)
@@ -1290,8 +1334,8 @@ func TestDeleteStorage(t *testing.T) {
}
root, _ := state.Commit(0, true, false)
// Init phase done, create two states, one with snap and one without
- fastState, _ := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps))
- slowState, _ := New(root, NewDatabase(tdb, nil))
+ fastState, _ := New(root, NewMPTDatabase(tdb, nil).WithSnapshot(snaps))
+ slowState, _ := New(root, NewMPTDatabase(tdb, nil))
obj := fastState.getOrNewStateObject(addr)
storageRoot := obj.data.Root
@@ -1368,3 +1412,38 @@ func TestStorageDirtiness(t *testing.T) {
state.RevertToSnapshot(snap)
checkDirty(common.Hash{0x1}, common.Hash{0x1}, true)
}
+
+// TestStateDBCopyUBT exercises StateDB.Copy on a UBT-backed state database.
+// Before the mustCopyTrie fix, this panicked with "unknown trie type
+// *bintrie.BinaryTrie" because the type switch in mustCopyTrie only covered
+// *trie.StateTrie and *transitiontrie.TransitionTrie.
+func TestStateDBCopyUBT(t *testing.T) {
+ disk := rawdb.NewMemoryDatabase()
+ tdb := triedb.NewDatabase(disk, triedb.UBTDefaults)
+ sdb := NewDatabase(tdb, nil)
+
+ orig, err := New(types.EmptyRootHash, sdb)
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }
+
+ // Touch the trie so StateDB.Copy actually has to copy it.
+ addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
+ orig.SetBalance(addr, uint256.NewInt(1_000), tracing.BalanceChangeUnspecified)
+
+ // Must not panic.
+ cpy := orig.Copy()
+ if cpy == nil {
+ t.Fatal("Copy returned nil")
+ }
+
+ // The copy must be independent: mutating the copy does not affect the
+ // original. Use balance as an observable.
+ cpy.SetBalance(addr, uint256.NewInt(2_000), tracing.BalanceChangeUnspecified)
+ if got, want := orig.GetBalance(addr), uint256.NewInt(1_000); got.Cmp(want) != 0 {
+ t.Fatalf("original balance mutated through copy: got %s, want %s", got, want)
+ }
+ if got, want := cpy.GetBalance(addr), uint256.NewInt(2_000); got.Cmp(want) != 0 {
+ t.Fatalf("copy balance did not update: got %s, want %s", got, want)
+ }
+}
diff --git a/core/state/stateupdate.go b/core/state/stateupdate.go
index 1c171cbd5e..582bcc3ec8 100644
--- a/core/state/stateupdate.go
+++ b/core/state/stateupdate.go
@@ -26,139 +26,143 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie/trienode"
- "github.com/ethereum/go-ethereum/triedb"
)
-// contractCode represents contract bytecode along with its associated metadata.
-type contractCode struct {
- hash common.Hash // hash is the cryptographic hash of the current contract code.
- blob []byte // blob is the binary representation of the current contract code.
- originHash common.Hash // originHash is the cryptographic hash of the code before mutation.
+// ContractCode represents contract bytecode mutation along with its
+// associated metadata.
+type ContractCode struct {
+ Hash common.Hash // Hash is the cryptographic hash of the current contract code.
+ Blob []byte // Blob is the binary representation of the current contract code.
+ OriginHash common.Hash // OriginHash is the cryptographic hash of the code before mutation.
// Derived fields, populated only when state tracking is enabled.
- duplicate bool // duplicate indicates whether the updated code already exists.
- originBlob []byte // originBlob is the original binary representation of the contract code.
+ Duplicate bool // Duplicate indicates whether the updated code already exists.
+ OriginBlob []byte // OriginBlob is the original binary representation of the contract code.
}
-// accountDelete represents an operation for deleting an Ethereum account.
-type accountDelete struct {
- address common.Address // address is the unique account identifier
- origin []byte // origin is the original value of account data in slim-RLP encoding.
-
- // storages stores mutated slots, the value should be nil.
- storages map[common.Hash][]byte
-
- // storagesOrigin stores the original values of mutated slots in
- // prefix-zero-trimmed RLP format. The map key refers to the **HASH**
- // of the raw storage slot key.
- storagesOrigin map[common.Hash][]byte
+// AccountDelete represents a deletion operation for an Ethereum account.
+type AccountDelete struct {
+ Address common.Address // Address uniquely identifies the account.
+ Origin *types.StateAccount // Origin is the account state prior to deletion (never be null).
+ Storages map[common.Hash]common.Hash // Storages contains mutated storage slots.
+ StoragesOrigin map[common.Hash]common.Hash // StoragesOrigin holds original values of mutated slots; keys are hashes of raw storage slot keys.
}
-// accountUpdate represents an operation for updating an Ethereum account.
-type accountUpdate struct {
- address common.Address // address is the unique account identifier
- data []byte // data is the slim-RLP encoded account data.
- origin []byte // origin is the original value of account data in slim-RLP encoding.
- code *contractCode // code represents mutated contract code; nil means it's not modified.
- storages map[common.Hash][]byte // storages stores mutated slots in prefix-zero-trimmed RLP format.
+// AccountUpdate represents an update operation for an Ethereum account.
+type AccountUpdate struct {
+ Address common.Address // Address uniquely identifies the account.
+ Data *types.StateAccount // Data is the updated account state; nil indicates deletion.
+ Origin *types.StateAccount // Origin is the previous account state; nil indicates non-existence.
+ Code *ContractCode // Code contains updated contract code; nil if unchanged.
+ Storages map[common.Hash]common.Hash // Storages contains updated storage slots.
- // storagesOriginByKey and storagesOriginByHash both store the original values
- // of mutated slots in prefix-zero-trimmed RLP format. The difference is that
- // storagesOriginByKey uses the **raw** storage slot key as the map ID, while
- // storagesOriginByHash uses the **hash** of the storage slot key instead.
- storagesOriginByKey map[common.Hash][]byte
- storagesOriginByHash map[common.Hash][]byte
+ // StoragesOriginByKey and StoragesOriginByHash both record original values
+ // of mutated storage slots:
+ // - StoragesOriginByKey uses raw storage slot keys.
+ // - StoragesOriginByHash uses hashed storage slot keys.
+ StoragesOriginByKey map[common.Hash]common.Hash
+ StoragesOriginByHash map[common.Hash]common.Hash
}
-// stateUpdate represents the difference between two states resulting from state
+// StorageKeyEncoding specifies the encoding scheme of a storage key.
+type StorageKeyEncoding int
+
+const (
+ // StorageKeyHashed represents a hashed key (e.g. Keccak256).
+ StorageKeyHashed StorageKeyEncoding = iota
+
+ // StorageKeyPlain represents a raw (unhashed) key.
+ StorageKeyPlain
+)
+
+// StateUpdate represents the difference between two states resulting from state
// execution. It contains information about mutated contract codes, accounts,
// and storage slots, along with their original values.
-type stateUpdate struct {
- originRoot common.Hash // hash of the state before applying mutation
- root common.Hash // hash of the state after applying mutation
- blockNumber uint64 // Associated block number
+type StateUpdate struct {
+ OriginRoot common.Hash // Hash of the state before applying mutation
+ Root common.Hash // Hash of the state after applying mutation
+ BlockNumber uint64 // Associated block number
- accounts map[common.Hash][]byte // accounts stores mutated accounts in 'slim RLP' encoding
- accountsOrigin map[common.Address][]byte // accountsOrigin stores the original values of mutated accounts in 'slim RLP' encoding
+ // Accounts contains mutated accounts, keyed by address hash.
+ Accounts map[common.Hash]*types.StateAccount
- // storages stores mutated slots in 'prefix-zero-trimmed' RLP format.
- // The value is keyed by account hash and **storage slot key hash**.
- storages map[common.Hash]map[common.Hash][]byte
+ // Storages contains mutated storage slots, keyed by address
+ // hash and storage slot key hash.
+ Storages map[common.Hash]map[common.Hash]common.Hash
- // storagesOrigin stores the original values of mutated slots in
- // 'prefix-zero-trimmed' RLP format.
- // (a) the value is keyed by account hash and **storage slot key** if rawStorageKey is true;
- // (b) the value is keyed by account hash and **storage slot key hash** if rawStorageKey is false;
- storagesOrigin map[common.Address]map[common.Hash][]byte
- rawStorageKey bool
+ // AccountsOrigin holds the original values of mutated accounts, keyed by address.
+ AccountsOrigin map[common.Address]*types.StateAccount
- codes map[common.Address]*contractCode // codes contains the set of dirty codes
- nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes
+ // StoragesOrigin holds the original values of mutated storage slots.
+ // The key format depends on StorageKeyType:
+ // - if StorageKeyType is plain: keyed by account address and plain storage slot key.
+ // - if StorageKeyType is hashed: keyed by account address and storage slot key hash.
+ StoragesOrigin map[common.Address]map[common.Hash]common.Hash
+ StorageKeyType StorageKeyEncoding
+
+ Codes map[common.Address]*ContractCode // Codes contains the set of dirty codes
+ Nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes
}
-// empty returns a flag indicating the state transition is empty or not.
-func (sc *stateUpdate) empty() bool {
- return sc.originRoot == sc.root
+// Empty returns a flag indicating the state transition is empty or not.
+func (sc *StateUpdate) Empty() bool {
+ return sc.OriginRoot == sc.Root
}
-// newStateUpdate constructs a state update object by identifying the differences
+// NewStateUpdate constructs a state update object by identifying the differences
// between two states through state execution. It combines the specified account
// deletions and account updates to create a complete state update.
-//
-// rawStorageKey is a flag indicating whether to use the raw storage slot key or
-// the hash of the slot key for constructing state update object.
-func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate {
+func NewStateUpdate(typ StorageKeyEncoding, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*AccountDelete, updates map[common.Hash]*AccountUpdate, nodes *trienode.MergedNodeSet) *StateUpdate {
var (
- accounts = make(map[common.Hash][]byte)
- accountsOrigin = make(map[common.Address][]byte)
- storages = make(map[common.Hash]map[common.Hash][]byte)
- storagesOrigin = make(map[common.Address]map[common.Hash][]byte)
- codes = make(map[common.Address]*contractCode)
+ accounts = make(map[common.Hash]*types.StateAccount)
+ accountsOrigin = make(map[common.Address]*types.StateAccount)
+ storages = make(map[common.Hash]map[common.Hash]common.Hash)
+ storagesOrigin = make(map[common.Address]map[common.Hash]common.Hash)
+ codes = make(map[common.Address]*ContractCode)
)
- // Since some accounts might be destroyed and recreated within the same
+ // Since some accounts might be deleted and recreated within the same
// block, deletions must be aggregated first.
for addrHash, op := range deletes {
- addr := op.address
+ addr := op.Address
accounts[addrHash] = nil
- accountsOrigin[addr] = op.origin
+ accountsOrigin[addr] = op.Origin
- // If storage wiping exists, the hash of the storage slot key must be used
- if len(op.storages) > 0 {
- storages[addrHash] = op.storages
+ if len(op.Storages) > 0 {
+ storages[addrHash] = op.Storages
}
- if len(op.storagesOrigin) > 0 {
- storagesOrigin[addr] = op.storagesOrigin
+ if len(op.StoragesOrigin) > 0 {
+ storagesOrigin[addr] = op.StoragesOrigin
}
}
// Aggregate account updates then.
for addrHash, op := range updates {
// Aggregate dirty contract codes if they are available.
- addr := op.address
- if op.code != nil {
- codes[addr] = op.code
+ addr := op.Address
+ if op.Code != nil {
+ codes[addr] = op.Code
}
- accounts[addrHash] = op.data
+ accounts[addrHash] = op.Data
// Aggregate the account original value. If the account is already
- // present in the aggregated accountsOrigin set, skip it.
+ // present in the aggregated AccountsOrigin set, skip it.
if _, found := accountsOrigin[addr]; !found {
- accountsOrigin[addr] = op.origin
+ accountsOrigin[addr] = op.Origin
}
// Aggregate the storage mutation list. If a slot in op.storages is
// already present in aggregated storages set, the value will be
// overwritten.
- if len(op.storages) > 0 {
+ if len(op.Storages) > 0 {
if _, exist := storages[addrHash]; !exist {
- storages[addrHash] = op.storages
+ storages[addrHash] = op.Storages
} else {
- maps.Copy(storages[addrHash], op.storages)
+ maps.Copy(storages[addrHash], op.Storages)
}
}
// Aggregate the storage original values. If the slot is already present
- // in aggregated storagesOrigin set, skip it.
- storageOriginSet := op.storagesOriginByHash
- if rawStorageKey {
- storageOriginSet = op.storagesOriginByKey
+ // in aggregated StoragesOrigin set, skip it.
+ storageOriginSet := op.StoragesOriginByHash
+ if typ == StorageKeyPlain {
+ storageOriginSet = op.StoragesOriginByKey
}
if len(storageOriginSet) > 0 {
origin, exist := storagesOrigin[addr]
@@ -173,32 +177,114 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash
}
}
}
- return &stateUpdate{
- originRoot: originRoot,
- root: root,
- blockNumber: blockNumber,
- accounts: accounts,
- accountsOrigin: accountsOrigin,
- storages: storages,
- storagesOrigin: storagesOrigin,
- rawStorageKey: rawStorageKey,
- codes: codes,
- nodes: nodes,
+ return &StateUpdate{
+ OriginRoot: originRoot,
+ Root: root,
+ BlockNumber: blockNumber,
+ Accounts: accounts,
+ AccountsOrigin: accountsOrigin,
+ Storages: storages,
+ StoragesOrigin: storagesOrigin,
+ StorageKeyType: typ,
+ Codes: codes,
+ Nodes: nodes,
}
}
-// stateSet converts the current stateUpdate object into a triedb.StateSet
-// object. This function extracts the necessary data from the stateUpdate
-// struct and formats it into the StateSet structure consumed by the triedb
-// package.
-func (sc *stateUpdate) stateSet() *triedb.StateSet {
- return &triedb.StateSet{
- Accounts: sc.accounts,
- AccountsOrigin: sc.accountsOrigin,
- Storages: sc.storages,
- StoragesOrigin: sc.storagesOrigin,
- RawStorageKey: sc.rawStorageKey,
+// encodeSlot encodes the storage slot value by trimming all leading zeros
+// and then RLP-encoding the result.
+func encodeSlot(value common.Hash) []byte {
+ if value == (common.Hash{}) {
+ return nil
}
+ blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
+ return blob
+}
+
+// EncodeMPTState encodes all state mutations alongside their original value
+// into the Merkle-Patricia-Trie representation.
+//
+// It transforms account and storage updates into their corresponding MPT-encoded
+// key-value mappings, using the same encoding rules as the Ethereum state trie.
+func (sc *StateUpdate) EncodeMPTState() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte) {
+ var (
+ accounts = make(map[common.Hash][]byte, len(sc.Accounts))
+ storages = make(map[common.Hash]map[common.Hash][]byte, len(sc.Storages))
+ accountOrigin = make(map[common.Address][]byte, len(sc.AccountsOrigin))
+ storageOrigin = make(map[common.Address]map[common.Hash][]byte, len(sc.StoragesOrigin))
+ )
+ for addr, prev := range sc.AccountsOrigin {
+ if prev == nil {
+ accountOrigin[addr] = nil
+ } else {
+ accountOrigin[addr] = types.SlimAccountRLP(*prev)
+ }
+ }
+ for addrHash, data := range sc.Accounts {
+ if data == nil {
+ accounts[addrHash] = nil
+ } else {
+ accounts[addrHash] = types.SlimAccountRLP(*data)
+ }
+ }
+ for addr, slots := range sc.StoragesOrigin {
+ subset := make(map[common.Hash][]byte)
+ for key, val := range slots {
+ subset[key] = encodeSlot(val)
+ }
+ storageOrigin[addr] = subset
+ }
+ for addrHash, slots := range sc.Storages {
+ subset := make(map[common.Hash][]byte)
+ for key, val := range slots {
+ subset[key] = encodeSlot(val)
+ }
+ storages[addrHash] = subset
+ }
+ return accounts, accountOrigin, storages, storageOrigin
+}
+
+// EncodeUBTState encodes all state mutations alongside their original value
+// into the Unified-Binary-Trie representation.
+//
+// It transforms account and storage updates into their corresponding UBT-encoded
+// key-value mappings, using the same encoding rules as the Ethereum state trie.
+func (sc *StateUpdate) EncodeUBTState() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte) {
+ var (
+ accounts = make(map[common.Hash][]byte, len(sc.Accounts))
+ storages = make(map[common.Hash]map[common.Hash][]byte, len(sc.Storages))
+ accountOrigin = make(map[common.Address][]byte, len(sc.AccountsOrigin))
+ storageOrigin = make(map[common.Address]map[common.Hash][]byte, len(sc.StoragesOrigin))
+ )
+ for addr, prev := range sc.AccountsOrigin {
+ if prev == nil {
+ accountOrigin[addr] = nil
+ } else {
+ accountOrigin[addr] = types.SlimAccountRLP(*prev)
+ }
+ }
+ for addrHash, data := range sc.Accounts {
+ if data == nil {
+ accounts[addrHash] = nil
+ } else {
+ accounts[addrHash] = types.SlimAccountRLP(*data)
+ }
+ }
+ for addr, slots := range sc.StoragesOrigin {
+ subset := make(map[common.Hash][]byte)
+ for key, val := range slots {
+ subset[key] = encodeSlot(val)
+ }
+ storageOrigin[addr] = subset
+ }
+ for addrHash, slots := range sc.Storages {
+ subset := make(map[common.Hash][]byte)
+ for key, val := range slots {
+ subset[key] = encodeSlot(val)
+ }
+ storages[addrHash] = subset
+ }
+ return accounts, accountOrigin, storages, storageOrigin
}
// deriveCodeFields derives the missing fields of contract code changes
@@ -207,135 +293,96 @@ func (sc *stateUpdate) stateSet() *triedb.StateSet {
// Note: This operation is expensive and not needed during normal state
// transitions. It is only required when SizeTracker or StateUpdate hook
// is enabled to produce accurate state statistics.
-func (sc *stateUpdate) deriveCodeFields(reader ContractCodeReader) error {
+func (sc *StateUpdate) deriveCodeFields(reader ContractCodeReader) error {
cache := make(map[common.Hash]bool)
- for addr, code := range sc.codes {
- if code.originHash != types.EmptyCodeHash {
- blob := reader.Code(addr, code.originHash)
+ for addr, code := range sc.Codes {
+ if code.OriginHash != types.EmptyCodeHash {
+ blob := reader.Code(addr, code.OriginHash)
if len(blob) == 0 {
return fmt.Errorf("original code of %x is empty", addr)
}
- code.originBlob = blob
+ code.OriginBlob = blob
}
- if exists, ok := cache[code.hash]; ok {
- code.duplicate = exists
+ if exists, ok := cache[code.Hash]; ok {
+ code.Duplicate = exists
continue
}
- res := reader.Has(addr, code.hash)
- cache[code.hash] = res
- code.duplicate = res
+ res := reader.Has(addr, code.Hash)
+ cache[code.Hash] = res
+ code.Duplicate = res
}
return nil
}
-// ToTracingUpdate converts the internal stateUpdate to an exported tracing.StateUpdate.
-func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
+// ToTracingUpdate converts the internal StateUpdate to an exported tracing.StateUpdate.
+func (sc *StateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
update := &tracing.StateUpdate{
- OriginRoot: sc.originRoot,
- Root: sc.root,
- BlockNumber: sc.blockNumber,
- AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.accountsOrigin)),
+ OriginRoot: sc.OriginRoot,
+ Root: sc.Root,
+ BlockNumber: sc.BlockNumber,
+ AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.AccountsOrigin)),
StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange),
- CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.codes)),
+ CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.Codes)),
TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange),
}
// Gather all account changes
- for addr, oldData := range sc.accountsOrigin {
+ for addr, oldData := range sc.AccountsOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes())
- newData, exists := sc.accounts[addrHash]
+ newData, exists := sc.Accounts[addrHash]
if !exists {
return nil, fmt.Errorf("account %x not found", addr)
}
- change := &tracing.AccountChange{}
-
- if len(oldData) > 0 {
- acct, err := types.FullAccount(oldData)
- if err != nil {
- return nil, err
- }
- change.Prev = &types.StateAccount{
- Nonce: acct.Nonce,
- Balance: acct.Balance,
- Root: acct.Root,
- CodeHash: acct.CodeHash,
- }
- }
- if len(newData) > 0 {
- acct, err := types.FullAccount(newData)
- if err != nil {
- return nil, err
- }
- change.New = &types.StateAccount{
- Nonce: acct.Nonce,
- Balance: acct.Balance,
- Root: acct.Root,
- CodeHash: acct.CodeHash,
- }
+ change := &tracing.AccountChange{
+ Prev: oldData,
+ New: newData,
}
update.AccountChanges[addr] = change
}
// Gather all storage slot changes
- for addr, slots := range sc.storagesOrigin {
+ for addr, slots := range sc.StoragesOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes())
- subset, exists := sc.storages[addrHash]
+ subset, exists := sc.Storages[addrHash]
if !exists {
return nil, fmt.Errorf("storage %x not found", addr)
}
storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots))
- for key, encPrev := range slots {
+ for key, oldData := range slots {
// Get new value - handle both raw and hashed key formats
var (
exists bool
- encNew []byte
- decPrev []byte
- decNew []byte
- err error
+ newData common.Hash
)
- if sc.rawStorageKey {
- encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())]
+ if sc.StorageKeyType == StorageKeyPlain {
+ newData, exists = subset[crypto.Keccak256Hash(key.Bytes())]
} else {
- encNew, exists = subset[key]
+ newData, exists = subset[key]
}
if !exists {
return nil, fmt.Errorf("storage slot %x-%x not found", addr, key)
}
-
- // Decode the prev and new values
- if len(encPrev) > 0 {
- _, decPrev, _, err = rlp.Split(encPrev)
- if err != nil {
- return nil, fmt.Errorf("failed to decode prevValue: %v", err)
- }
- }
- if len(encNew) > 0 {
- _, decNew, _, err = rlp.Split(encNew)
- if err != nil {
- return nil, fmt.Errorf("failed to decode newValue: %v", err)
- }
- }
storageChanges[key] = &tracing.StorageChange{
- Prev: common.BytesToHash(decPrev),
- New: common.BytesToHash(decNew),
+ Prev: oldData,
+ New: newData,
}
}
update.StorageChanges[addr] = storageChanges
}
// Gather all contract code changes
- for addr, code := range sc.codes {
+ for addr, code := range sc.Codes {
change := &tracing.CodeChange{
New: &tracing.ContractCode{
- Hash: code.hash,
- Code: code.blob,
- Exists: code.duplicate,
+ Hash: code.Hash,
+ Code: code.Blob,
+ Exists: code.Duplicate,
},
}
- if code.originHash != types.EmptyCodeHash {
+ if code.OriginHash != types.EmptyCodeHash {
change.Prev = &tracing.ContractCode{
- Hash: code.originHash,
- Code: code.originBlob,
+ Hash: code.OriginHash,
+ Code: code.OriginBlob,
Exists: true,
}
}
@@ -343,8 +390,8 @@ func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
}
// Gather all trie node changes
- if sc.nodes != nil {
- for owner, subset := range sc.nodes.Sets {
+ if sc.Nodes != nil {
+ for owner, subset := range sc.Nodes.Sets {
nodeChanges := make(map[string]*tracing.TrieNodeChange, len(subset.Origins))
for path, oldNode := range subset.Origins {
newNode, exists := subset.Nodes[path]
diff --git a/core/state/transient_storage.go b/core/state/transient_storage.go
index 3bb4955425..a3cfaceb3e 100644
--- a/core/state/transient_storage.go
+++ b/core/state/transient_storage.go
@@ -25,8 +25,13 @@ import (
"github.com/ethereum/go-ethereum/common"
)
+type transientStorageKey struct {
+ addr common.Address
+ key common.Hash
+}
+
// transientStorage is a representation of EIP-1153 "Transient Storage".
-type transientStorage map[common.Address]Storage
+type transientStorage map[transientStorageKey]common.Hash
// newTransientStorage creates a new instance of a transientStorage.
func newTransientStorage() transientStorage {
@@ -35,52 +40,43 @@ func newTransientStorage() transientStorage {
// Set sets the transient-storage `value` for `key` at the given `addr`.
func (t transientStorage) Set(addr common.Address, key, value common.Hash) {
+ tsKey := transientStorageKey{addr: addr, key: key}
if value == (common.Hash{}) { // this is a 'delete'
- if _, ok := t[addr]; ok {
- delete(t[addr], key)
- if len(t[addr]) == 0 {
- delete(t, addr)
- }
- }
+ delete(t, tsKey)
} else {
- if _, ok := t[addr]; !ok {
- t[addr] = make(Storage)
- }
- t[addr][key] = value
+ t[tsKey] = value
}
}
// Get gets the transient storage for `key` at the given `addr`.
func (t transientStorage) Get(addr common.Address, key common.Hash) common.Hash {
- val, ok := t[addr]
- if !ok {
- return common.Hash{}
- }
- return val[key]
+ tsKey := transientStorageKey{addr: addr, key: key}
+ return t[tsKey]
}
// Copy does a deep copy of the transientStorage
func (t transientStorage) Copy() transientStorage {
- storage := make(transientStorage)
- for key, value := range t {
- storage[key] = value.Copy()
- }
- return storage
+ return maps.Clone(t)
}
// PrettyPrint prints the contents of the access list in a human-readable form
func (t transientStorage) PrettyPrint() string {
out := new(strings.Builder)
- sortedAddrs := slices.Collect(maps.Keys(t))
- slices.SortFunc(sortedAddrs, common.Address.Cmp)
+ sortedTSKeys := slices.Collect(maps.Keys(t))
+ slices.SortFunc(sortedTSKeys, func(a, b transientStorageKey) int {
+ r := a.addr.Cmp(b.addr)
+ if r != 0 {
+ return r
+ }
+ return a.key.Cmp(b.key)
+ })
- for _, addr := range sortedAddrs {
- fmt.Fprintf(out, "%#x:", addr)
- storage := t[addr]
- sortedKeys := slices.Collect(maps.Keys(storage))
- slices.SortFunc(sortedKeys, common.Hash.Cmp)
- for _, key := range sortedKeys {
- fmt.Fprintf(out, " %X : %X\n", key, storage[key])
+ for i := 0; i < len(sortedTSKeys); {
+ tsKey := sortedTSKeys[i]
+ fmt.Fprintf(out, "%#x:", tsKey.addr)
+ for ; i < len(sortedTSKeys) && sortedTSKeys[i].addr == tsKey.addr; i++ {
+ tsKey2 := sortedTSKeys[i]
+ fmt.Fprintf(out, " %X : %X\n", tsKey2.key, t[tsKey2])
}
}
return out.String()
diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go
index a9faddcdff..a0310eb3b3 100644
--- a/core/state/trie_prefetcher.go
+++ b/core/state/trie_prefetcher.go
@@ -40,7 +40,7 @@ var (
//
// Note, the prefetcher's API is not thread safe.
type triePrefetcher struct {
- verkle bool // Flag whether the prefetcher is in verkle mode
+ isUBT bool // Flag whether the prefetcher is in UBT mode
db Database // Database to fetch trie nodes through
root common.Hash // Root hash of the account trie for metrics
fetchers map[string]*subfetcher // Subfetchers for each trie
@@ -67,7 +67,7 @@ type triePrefetcher struct {
func newTriePrefetcher(db Database, root common.Hash, namespace string, noreads bool) *triePrefetcher {
prefix := triePrefetchMetricsPrefix + namespace
return &triePrefetcher{
- verkle: db.TrieDB().IsVerkle(),
+ isUBT: db.Type().Is(TypeUBT),
db: db,
root: root,
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
@@ -206,8 +206,8 @@ func (p *triePrefetcher) used(owner common.Hash, root common.Hash, usedAddr []co
// trieID returns an unique trie identifier consists the trie owner and root hash.
func (p *triePrefetcher) trieID(owner common.Hash, root common.Hash) string {
- // The trie in verkle is only identified by state root
- if p.verkle {
+ // The trie in ubt is only identified by state root
+ if p.isUBT {
return p.root.Hex()
}
// The trie in merkle is either identified by state root (account trie),
@@ -340,12 +340,12 @@ func (sf *subfetcher) terminate(async bool) {
// openTrie resolves the target trie from database for prefetching.
func (sf *subfetcher) openTrie() error {
- // Open the verkle tree if the sub-fetcher is in verkle mode. Note, there is
- // only a single fetcher for verkle.
- if sf.db.TrieDB().IsVerkle() {
+ // Open the ubt tree if the sub-fetcher is in ubt mode. Note, there is
+ // only a single fetcher for ubt.
+ if sf.db.Type().Is(TypeUBT) {
tr, err := sf.db.OpenTrie(sf.state)
if err != nil {
- log.Warn("Trie prefetcher failed opening verkle trie", "root", sf.root, "err", err)
+ log.Warn("Trie prefetcher failed opening UBT trie", "root", sf.root, "err", err)
return err
}
sf.trie = tr
diff --git a/core/state/trie_prefetcher_test.go b/core/state/trie_prefetcher_test.go
index 41349c0c0e..8a03d93a08 100644
--- a/core/state/trie_prefetcher_test.go
+++ b/core/state/trie_prefetcher_test.go
@@ -68,7 +68,7 @@ func TestUseAfterTerminate(t *testing.T) {
func TestVerklePrefetcher(t *testing.T) {
disk := rawdb.NewMemoryDatabase()
- db := triedb.NewDatabase(disk, triedb.VerkleDefaults)
+ db := triedb.NewDatabase(disk, triedb.UBTDefaults)
sdb := NewDatabase(db, nil)
state, err := New(types.EmptyRootHash, sdb)
@@ -86,18 +86,17 @@ func TestVerklePrefetcher(t *testing.T) {
root, _ := state.Commit(0, true, false)
state, _ = New(root, sdb)
- sRoot := state.GetStorageRoot(addr)
fetcher := newTriePrefetcher(sdb, root, "", false)
// Read account
fetcher.prefetch(common.Hash{}, root, common.Address{}, []common.Address{addr}, nil, false)
// Read storage slot
- fetcher.prefetch(crypto.Keccak256Hash(addr.Bytes()), sRoot, addr, nil, []common.Hash{skey}, false)
+ fetcher.prefetch(crypto.Keccak256Hash(addr.Bytes()), common.Hash{}, addr, nil, []common.Hash{skey}, false)
fetcher.terminate(false)
accountTrie := fetcher.trie(common.Hash{}, root)
- storageTrie := fetcher.trie(crypto.Keccak256Hash(addr.Bytes()), sRoot)
+ storageTrie := fetcher.trie(crypto.Keccak256Hash(addr.Bytes()), common.Hash{})
rootA := accountTrie.Hash()
rootB := storageTrie.Hash()
diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go
index c91d40d94f..d99611ff2c 100644
--- a/core/state_prefetcher.go
+++ b/core/state_prefetcher.go
@@ -93,6 +93,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
}
// Execute the message to preload the implicit touched states
evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg)
+ defer evm.Release()
// Convert the transaction into an executable message and pre-cache its sender
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
@@ -103,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 bbb1341299..13466b7815 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -22,6 +22,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
@@ -30,6 +31,8 @@ import (
"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
@@ -62,7 +65,7 @@ func (p *StateProcessor) chainConfig() *params.ChainConfig {
func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) {
var (
config = p.chainConfig()
- receipts types.Receipts
+ receipts = make(types.Receipts, 0, len(block.Transactions()))
header = block.Header()
blockHash = block.Hash()
blockNumber = block.Number()
@@ -73,34 +76,25 @@ 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
+ context = NewEVMBlockContext(header, p.chain, nil)
signer = types.MakeSigner(config, header.Number, header.Time)
+ evm = vm.NewEVM(context, tracingStateDB, config, cfg)
)
-
- // Apply pre-execution system calls.
- context = NewEVMBlockContext(header, p.chain, nil)
- evm := vm.NewEVM(context, tracingStateDB, config, cfg)
-
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if config.IsPrague(block.Number(), block.Time()) || config.IsVerkle(block.Number(), block.Time()) {
- ProcessParentBlockHash(block.ParentHash(), evm)
- }
-
+ defer evm.Release()
+ // Run the pre-execution system calls
+ 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() {
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
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)),
@@ -115,11 +109,10 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
allLogs = append(allLogs, receipt.Logs...)
spanEnd(nil)
}
- requests, err := postExecution(ctx, config, block, allLogs, evm)
+ requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1))
if err != nil {
return nil, err
}
-
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body())
@@ -131,28 +124,44 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated
}, 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) {
+ _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution")
+ defer spanEnd(nil)
+
+ // EIP-4788
+ if beaconRoot != nil {
+ ProcessBeaconBlockRoot(*beaconRoot, evm)
+ }
+ // EIP-2935
+ if config.IsPrague(number, time) || config.IsUBT(number, time) {
+ ProcessParentBlockHash(parent, evm)
+ }
+}
+
+// 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, err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution")
defer spanEnd(&err)
// Read requests if Prague is enabled.
- if config.IsPrague(block.Number(), block.Time()) {
+ if config.IsPrague(number, time) {
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, 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, evm, blockAccessIndex); err != nil {
+ return 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, evm, blockAccessIndex); err != nil {
+ return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
}
-
return requests, nil
}
@@ -182,7 +191,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
}
// Merge the tx-local access event into the "block-local" one, in order to collect
// all values, so that the witness can be built.
- if statedb.Database().TrieDB().IsVerkle() {
+ 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
@@ -251,15 +260,16 @@ 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.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
- _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
+ _, _, _ = 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)
}
@@ -278,15 +288,16 @@ 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.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
- _, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
+ _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
if err != nil {
panic(err)
}
@@ -298,17 +309,17 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
// 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, evm *vm.EVM, blockAccessIndex uint32) error {
+ return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex)
}
// 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, evm *vm.EVM, blockAccessIndex uint32) error {
+ return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex)
}
-func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error {
+func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
@@ -318,14 +329,15 @@ 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.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr)
- ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
+ 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)
}
@@ -372,3 +384,11 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) {
tracer.OnSystemCallStart()
}
}
+
+// 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)
+ header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
+ return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
+}
diff --git a/core/state_transition.go b/core/state_transition.go
index bd7e5daeff..0a6994505d 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) (uint64, 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 {
@@ -89,46 +89,110 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
- return 0, ErrGasUintOverflow
+ return vm.GasCosts{}, ErrGasUintOverflow
}
gas += nz * nonZeroGas
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
- return 0, ErrGasUintOverflow
+ return vm.GasCosts{}, ErrGasUintOverflow
}
gas += z * params.TxDataZeroGas
if isContractCreation && isEIP3860 {
lenWords := toWordSize(dataLen)
if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords {
- return 0, ErrGasUintOverflow
+ return vm.GasCosts{}, ErrGasUintOverflow
}
gas += lenWords * params.InitCodeWordGas
}
}
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
}
- return gas, nil
+ return vm.GasCosts{RegularGas: gas}, nil
}
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
-func FloorDataGas(data []byte) (uint64, error) {
+func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) {
var (
- z = uint64(bytes.Count(data, []byte{0}))
- nz = uint64(len(data)) - z
- tokens = nz*params.TxTokenPerNonZeroByte + z
+ tokens uint64
+ tokenCost uint64
)
+ if rules.IsAmsterdam {
+ // EIP-7976 changes how calldata is priced.
+ // From 10/40 to 64/64 for zero/non-zero bytes.
+ 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
+ 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
+ }
+
// Check for overflow
- if (math.MaxUint64-params.TxGas)/params.TxCostFloorPerToken < tokens {
+ if (math.MaxUint64-params.TxGas)/tokenCost < tokens {
return 0, ErrGasUintOverflow
}
// Minimum gas required for a transaction based on its data tokens (EIP-7623).
- return params.TxGas + tokens*params.TxCostFloorPerToken, nil
+ return params.TxGas + tokens*tokenCost, nil
}
// toWordSize returns the ceiled word size required for init code payment calculation.
@@ -146,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
@@ -174,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
@@ -242,12 +338,12 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
// 5. Run Script section
// 6. Derive new state root
type stateTransition struct {
- gp *GasPool
- msg *Message
- gasRemaining uint64
- initialGas uint64
- state vm.StateDB
- evm *vm.EVM
+ gp *GasPool
+ msg *Message
+ initialBudget vm.GasBudget
+ gasRemaining vm.GasBudget
+ state vm.StateDB
+ evm *vm.EVM
}
// newStateTransition initialises and returns a new state transition object.
@@ -269,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 = st.msg.GasLimit
- st.initialGas = st.msg.GasLimit
+ 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
}
@@ -330,9 +450,10 @@ func (st *stateTransition) preCheck() error {
}
}
isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap.
- if isOsaka && msg.GasLimit > params.MaxTxGas {
+ if isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
// Make sure the sender is an EOA
@@ -347,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)
}
@@ -395,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)
}
@@ -446,18 +559,21 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
contractCreation = msg.To == nil
floorDataGas uint64
)
-
// Check clauses 4-5, subtract intrinsic gas if everything is correct
- gas, 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
}
- if st.gasRemaining < gas {
- return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
+ prior, sufficient := st.gasRemaining.Charge(cost)
+ if !sufficient {
+ return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas)
+ }
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas)
}
// Gas limit suffices for the floor data cost (EIP-7623)
if rules.IsPrague {
- floorDataGas, err = FloorDataGas(msg.Data)
+ floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList)
if err != nil {
return nil, err
}
@@ -465,10 +581,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas)
}
}
- if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
- t.OnGasChange(st.gasRemaining, st.gasRemaining-gas, tracing.GasChangeTxIntrinsicGas)
- }
- st.gasRemaining -= gas
if rules.IsEIP4762 {
st.evm.AccessEvents.AddTxOrigin(msg.From)
@@ -479,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())
@@ -535,14 +647,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
peakGasUsed := st.gasUsed()
// Compute refund counter, capped to a refund quotient.
- st.gasRemaining += st.calcRefund()
+ st.gasRemaining.Refund(st.calcRefund())
+
if rules.IsPrague {
// After EIP-7623: Data-heavy transactions pay the floor gas.
- if st.gasUsed() < floorDataGas {
- prev := st.gasRemaining
- st.gasRemaining = st.initialGas - floorDataGas
- if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
- t.OnGasChange(prev, st.gasRemaining, tracing.GasChangeTxDataFloor)
+ if used := st.gasUsed(); used < floorDataGas {
+ prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used})
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor)
}
}
if peakGasUsed < floorDataGas {
@@ -555,19 +667,22 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// Return gas to the gas pool
if rules.IsAmsterdam {
// Refund is excluded for returning
- err = st.gp.ReturnGas(st.initialGas-peakGasUsed, st.gasUsed())
+ err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed())
} else {
// Refund is included for returning
- err = st.gp.ReturnGas(st.gasRemaining, st.gasUsed())
+ err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed())
}
if err != nil {
return nil, err
}
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
@@ -575,7 +690,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
@@ -655,7 +770,7 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization)
}
// calcRefund computes refund counter, capped to a refund quotient.
-func (st *stateTransition) calcRefund() uint64 {
+func (st *stateTransition) calcRefund() vm.GasBudget {
var refund uint64
if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Before EIP-3529: refunds were capped to gasUsed / 2
@@ -667,27 +782,32 @@ func (st *stateTransition) calcRefund() uint64 {
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, st.gasRemaining+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 refund
+ return vm.NewGasBudget(refund)
}
// returnGas returns ETH for remaining gas,
// exchanged at the original rate.
func (st *stateTransition) returnGas() {
- remaining := uint256.NewInt(st.gasRemaining)
- remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
+ remaining := uint256.NewInt(st.gasRemaining.RegularGas)
+ 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 > 0 {
- st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 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)
}
}
// gasUsed returns the amount of gas used up by the state transition.
func (st *stateTransition) gasUsed() uint64 {
- return st.initialGas - st.gasRemaining
+ return st.gasRemaining.Used(st.initialBudget)
}
// blobGasUsed returns the amount of blob gas used by the message.
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 de63689bc5..6ea3f7ebbf 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -164,9 +164,37 @@ 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)
+ // 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 -
*/
@@ -248,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
@@ -278,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
@@ -333,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 7155a67a9b..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)
@@ -441,9 +552,9 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
// Initialize the state with head block, or fallback to empty one in
// case the head state is not available (might occur when node is not
// fully synced).
- state, err := p.chain.StateAt(head.Root)
+ state, err := p.chain.StateAt(head)
if err != nil {
- state, err = p.chain.StateAt(types.EmptyRootHash)
+ state, err = p.chain.StateAt(p.chain.Genesis().Header())
}
if err != nil {
return err
@@ -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
}
@@ -894,7 +1050,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
// Handle reorg buffer timeouts evicting old gapped transactions
p.evictGapped()
- statedb, err := p.chain.StateAt(newHead.Root)
+ statedb, err := p.chain.StateAt(newHead)
if err != nil {
log.Error("Failed to reset blobpool state", "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 ba96bea8ed..8032e21e8a 100644
--- a/core/txpool/blobpool/blobpool_test.go
+++ b/core/txpool/blobpool/blobpool_test.go
@@ -45,6 +45,7 @@ import (
"github.com/ethereum/go-ethereum/internal/testrand"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie"
"github.com/holiman/billy"
"github.com/holiman/uint256"
)
@@ -180,10 +181,14 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block
return bc.blocks[number]
}
-func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
+func (bc *testBlockChain) StateAt(header *types.Header) (*state.StateDB, error) {
return bc.statedb, nil
}
+func (bc *testBlockChain) Genesis() *types.Block {
+ return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil))
+}
+
// reserver is a utility struct to sanity check that accounts are
// properly reserved by the blobpool (no duplicate reserves or unreserves).
type reserver struct {
@@ -230,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 {
@@ -525,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 {
@@ -542,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{}{}
@@ -555,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{}{}
@@ -568,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 {
@@ -590,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{}{}
@@ -609,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 {
@@ -631,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{}{}
@@ -649,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 {
@@ -665,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 {
@@ -681,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)
@@ -700,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 {
@@ -837,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()
@@ -929,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}
@@ -1004,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}
@@ -1093,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
@@ -1196,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
@@ -1276,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 (
@@ -1741,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)
}
}
@@ -1848,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
@@ -2050,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/interface.go b/core/txpool/blobpool/interface.go
index 6f296a54bd..d7beae9b25 100644
--- a/core/txpool/blobpool/interface.go
+++ b/core/txpool/blobpool/interface.go
@@ -32,6 +32,9 @@ type BlockChain interface {
// CurrentBlock returns the current head of the chain.
CurrentBlock() *types.Header
+ // Genesis returns the genesis block of the chain.
+ Genesis() *types.Block
+
// CurrentFinalBlock returns the current block below which blobs should not
// be maintained anymore for reorg purposes.
CurrentFinalBlock() *types.Header
@@ -39,6 +42,6 @@ type BlockChain interface {
// GetBlock retrieves a specific block, used during pool resets.
GetBlock(hash common.Hash, number uint64) *types.Block
- // StateAt returns a state database for a given root hash (generally the head).
- StateAt(root common.Hash) (*state.StateDB, error)
+ // StateAt returns a state database for a given chain header (generally the head).
+ StateAt(header *types.Header) (*state.StateDB, error)
}
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 93b3cb5be2..3d66803fd7 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -129,11 +129,14 @@ type BlockChain interface {
// CurrentBlock returns the current head of the chain.
CurrentBlock() *types.Header
+ // Genesis returns the genesis block of the chain.
+ Genesis() *types.Block
+
// GetBlock retrieves a specific block, used during pool resets.
GetBlock(hash common.Hash, number uint64) *types.Block
- // StateAt returns a state database for a given root hash (generally the head).
- StateAt(root common.Hash) (*state.StateDB, error)
+ // StateAt returns a state database for a given chain header (generally the head).
+ StateAt(header *types.Header) (*state.StateDB, error)
}
// Config are the configuration parameters of the transaction pool.
@@ -317,9 +320,9 @@ func (pool *LegacyPool) Init(gasTip uint64, head *types.Header, reserver txpool.
// Initialize the state with head block, or fallback to empty one in
// case the head state is not available (might occur when node is not
// fully synced).
- statedb, err := pool.chain.StateAt(head.Root)
+ statedb, err := pool.chain.StateAt(head)
if err != nil {
- statedb, err = pool.chain.StateAt(types.EmptyRootHash)
+ statedb, err = pool.chain.StateAt(pool.chain.Genesis().Header())
}
if err != nil {
return err
@@ -464,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 {
@@ -500,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))
@@ -1219,8 +1222,10 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest,
pool.mu.Lock()
if reset != nil {
if reset.newHead != nil && reset.oldHead != nil {
- // Discard the transactions with the gas limit higher than the cap.
- if pool.chainconfig.IsOsaka(reset.newHead.Number, reset.newHead.Time) && !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) {
+ // Discard the transactions with the gas limit higher than the cap at the
+ // Osaka fork boundary.
+ if pool.chainconfig.IsOsaka(reset.newHead.Number, reset.newHead.Time) &&
+ !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) {
var hashes []common.Hash
pool.all.Range(func(hash common.Hash, tx *types.Transaction) bool {
if tx.Gas() > params.MaxTxGas {
@@ -1379,7 +1384,7 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) {
if newHead == nil {
newHead = pool.chain.CurrentBlock() // Special case during testing
}
- statedb, err := pool.chain.StateAt(newHead.Root)
+ statedb, err := pool.chain.StateAt(newHead)
if err != nil {
log.Error("Failed to reset txpool state", "err", err)
return
diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go
index fb994d8208..f8592ba001 100644
--- a/core/txpool/legacypool/legacypool_test.go
+++ b/core/txpool/legacypool/legacypool_test.go
@@ -91,10 +91,14 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block
return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil))
}
-func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
+func (bc *testBlockChain) StateAt(header *types.Header) (*state.StateDB, error) {
return bc.statedb, nil
}
+func (bc *testBlockChain) Genesis() *types.Block {
+ return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil))
+}
+
func (bc *testBlockChain) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
return bc.chainHeadFeed.Subscribe(ch)
}
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/locals/tx_tracker_test.go b/core/txpool/locals/tx_tracker_test.go
index dde8754605..34fb4d0b74 100644
--- a/core/txpool/locals/tx_tracker_test.go
+++ b/core/txpool/locals/tx_tracker_test.go
@@ -102,7 +102,7 @@ func (env *testEnv) setGasTip(gasTip uint64) {
func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
if nonce == 0 {
head := env.chain.CurrentHeader()
- state, _ := env.chain.StateAt(head.Root)
+ state, _ := env.chain.StateAt(head)
nonce = state.GetNonce(address)
}
if gasPrice == nil {
@@ -114,7 +114,7 @@ func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
func (env *testEnv) makeTxs(n int) []*types.Transaction {
head := env.chain.CurrentHeader()
- state, _ := env.chain.StateAt(head.Root)
+ state, _ := env.chain.StateAt(head)
nonce := state.GetNonce(address)
var txs []*types.Transaction
diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go
index 25647e0cce..9c78748422 100644
--- a/core/txpool/txpool.go
+++ b/core/txpool/txpool.go
@@ -50,11 +50,14 @@ type BlockChain interface {
// CurrentBlock returns the current head of the chain.
CurrentBlock() *types.Header
+ // Genesis returns the genesis block of the chain.
+ Genesis() *types.Block
+
// SubscribeChainHeadEvent subscribes to new blocks being added to the chain.
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
- // StateAt returns a state database for a given root hash (generally the head).
- StateAt(root common.Hash) (*state.StateDB, error)
+ // StateAt returns a state database for a given chain header (generally the head).
+ StateAt(header *types.Header) (*state.StateDB, error)
}
// TxPool is an aggregator for various transaction specific pools, collectively
@@ -87,9 +90,9 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) {
// Initialize the state with head block, or fallback to empty one in
// case the head state is not available (might occur when node is not
// fully synced).
- statedb, err := chain.StateAt(head.Root)
+ statedb, err := chain.StateAt(head)
if err != nil {
- statedb, err = chain.StateAt(types.EmptyRootHash)
+ statedb, err = chain.StateAt(chain.Genesis().Header())
}
if err != nil {
return nil, err
@@ -185,7 +188,7 @@ func (p *TxPool) loop(head *types.Header) {
case resetBusy <- struct{}{}:
// Updates the statedb with the new chain head. The head state may be
// unavailable if the initial state sync has not yet completed.
- if statedb, err := p.chain.StateAt(newHead.Root); err != nil {
+ if statedb, err := p.chain.StateAt(newHead); err != nil {
log.Error("Failed to reset txpool state", "err", err)
} else {
p.stateLock.Lock()
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index 13b1bfa312..c87bba31ac 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -92,7 +92,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
return err
}
}
- if rules.IsOsaka && tx.Gas() > params.MaxTxGas {
+ if rules.IsOsaka && !rules.IsAmsterdam && tx.Gas() > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas())
}
// Transactions can't be negative. This may never happen using RLP decoded
@@ -125,16 +125,16 @@ 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
}
- if tx.Gas() < intrGas {
- return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas)
+ if tx.Gas() < intrGas.RegularGas {
+ return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas.RegularGas)
}
// Ensure the transaction can cover floor data gas.
if rules.IsPrague {
- floorDataGas, err := core.FloorDataGas(tx.Data())
+ floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
return err
}
diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go
index 86dc8e5426..9cbc1faeb9 100644
--- a/core/types/bal/bal.go
+++ b/core/types/bal/bal.go
@@ -25,13 +25,13 @@ import (
)
// ConstructionAccountAccess contains post-block account state for mutations as well as
-// all storage keys that were read during execution. It is used when building block
+// all storage keys that were read during execution. It is used when building block
// access list during execution.
type ConstructionAccountAccess struct {
// StorageWrites is the post-state values of an account's storage slots
// that were modified in a block, keyed by the slot key and the tx index
// where the modification occurred.
- StorageWrites map[common.Hash]map[uint16]common.Hash `json:"storageWrites,omitempty"`
+ StorageWrites map[common.Hash]map[uint32]common.Hash `json:"storageWrites,omitempty"`
// StorageReads is the set of slot keys that were accessed during block
// execution.
@@ -42,25 +42,25 @@ type ConstructionAccountAccess struct {
// BalanceChanges contains the post-transaction balances of an account,
// keyed by transaction indices where it was changed.
- BalanceChanges map[uint16]*uint256.Int `json:"balanceChanges,omitempty"`
+ BalanceChanges map[uint32]*uint256.Int `json:"balanceChanges,omitempty"`
// NonceChanges contains the post-state nonce values of an account keyed
// by tx index.
- NonceChanges map[uint16]uint64 `json:"nonceChanges,omitempty"`
+ NonceChanges map[uint32]uint64 `json:"nonceChanges,omitempty"`
// CodeChange contains the post-state contract code of an account keyed
// by tx index.
- CodeChange map[uint16][]byte `json:"codeChange,omitempty"`
+ CodeChange map[uint32][]byte `json:"codeChange,omitempty"`
}
// NewConstructionAccountAccess initializes the account access object.
func NewConstructionAccountAccess() *ConstructionAccountAccess {
return &ConstructionAccountAccess{
- StorageWrites: make(map[common.Hash]map[uint16]common.Hash),
+ StorageWrites: make(map[common.Hash]map[uint32]common.Hash),
StorageReads: make(map[common.Hash]struct{}),
- BalanceChanges: make(map[uint16]*uint256.Int),
- NonceChanges: make(map[uint16]uint64),
- CodeChange: make(map[uint16][]byte),
+ BalanceChanges: make(map[uint32]*uint256.Int),
+ NonceChanges: make(map[uint32]uint64),
+ CodeChange: make(map[uint32][]byte),
}
}
@@ -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),
}
}
@@ -97,12 +97,12 @@ func (b *ConstructionBlockAccessList) StorageRead(address common.Address, key co
// StorageWrite records the post-transaction value of a mutated storage slot.
// The storage slot is removed from the list of read slots.
-func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.Address, key, value common.Hash) {
+func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint32, address common.Address, key, value common.Hash) {
if _, ok := b.Accounts[address]; !ok {
b.Accounts[address] = NewConstructionAccountAccess()
}
if _, ok := b.Accounts[address].StorageWrites[key]; !ok {
- b.Accounts[address].StorageWrites[key] = make(map[uint16]common.Hash)
+ b.Accounts[address].StorageWrites[key] = make(map[uint32]common.Hash)
}
b.Accounts[address].StorageWrites[key][txIdx] = value
@@ -110,7 +110,7 @@ func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.
}
// CodeChange records the code of a newly-created contract.
-func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint16, code []byte) {
+func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint32, code []byte) {
if _, ok := b.Accounts[address]; !ok {
b.Accounts[address] = NewConstructionAccountAccess()
}
@@ -120,7 +120,7 @@ func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex
// NonceChange records tx post-state nonce of any contract-like accounts whose
// nonce was incremented.
-func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint16, postNonce uint64) {
+func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint32, postNonce uint64) {
if _, ok := b.Accounts[address]; !ok {
b.Accounts[address] = NewConstructionAccountAccess()
}
@@ -129,7 +129,7 @@ func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx
// BalanceChange records the post-transaction balance of an account whose
// balance changed.
-func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint16, address common.Address, balance *uint256.Int) {
+func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common.Address, balance *uint256.Int) {
if _, ok := b.Accounts[address]; !ok {
b.Accounts[address] = NewConstructionAccountAccess()
}
@@ -148,26 +148,26 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList {
for addr, aa := range b.Accounts {
var aaCopy ConstructionAccountAccess
- slotWrites := make(map[common.Hash]map[uint16]common.Hash, len(aa.StorageWrites))
+ slotWrites := make(map[common.Hash]map[uint32]common.Hash, len(aa.StorageWrites))
for key, m := range aa.StorageWrites {
slotWrites[key] = maps.Clone(m)
}
aaCopy.StorageWrites = slotWrites
aaCopy.StorageReads = maps.Clone(aa.StorageReads)
- balances := make(map[uint16]*uint256.Int, len(aa.BalanceChanges))
+ balances := make(map[uint32]*uint256.Int, len(aa.BalanceChanges))
for index, balance := range aa.BalanceChanges {
balances[index] = balance.Clone()
}
aaCopy.BalanceChanges = balances
aaCopy.NonceChanges = maps.Clone(aa.NonceChanges)
- codes := make(map[uint16][]byte, len(aa.CodeChange))
+ codes := make(map[uint32][]byte, len(aa.CodeChange))
for index, code := range aa.CodeChange {
codes[index] = bytes.Clone(code)
}
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 6d52c17c83..03f97f3809 100644
--- a/core/types/bal/bal_encoding.go
+++ b/core/types/bal/bal_encoding.go
@@ -33,27 +33,59 @@ import (
"github.com/holiman/uint256"
)
-//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type BlockAccessList -decoder
+//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type AccountAccess -decoder
// These are objects used as input for the access list encoding. They mirror
// the spec format.
// BlockAccessList is the encoding format of ConstructionBlockAccessList.
-type BlockAccessList struct {
- Accesses []AccountAccess `ssz-max:"300000"`
+type BlockAccessList []AccountAccess
+
+// EncodeRLP implements rlp.Encoder. It encodes the access list as a single
+// RLP list of AccountAccess entries.
+func (e BlockAccessList) EncodeRLP(w io.Writer) error {
+ buf := rlp.NewEncoderBuffer(w)
+ l := buf.List()
+ for i := range e {
+ if err := e[i].EncodeRLP(buf); err != nil {
+ return err
+ }
+ }
+ buf.ListEnd(l)
+ return buf.Flush()
+}
+
+// DecodeRLP implements rlp.Decoder.
+func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error {
+ if _, err := s.List(); err != nil {
+ return err
+ }
+ var list BlockAccessList
+ for s.MoreDataInList() {
+ var a AccountAccess
+ if err := a.DecodeRLP(s); err != nil {
+ return err
+ }
+ list = append(list, a)
+ }
+ if err := s.ListEnd(); err != nil {
+ return err
+ }
+ *e = list
+ return nil
}
// 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() error {
- if !slices.IsSortedFunc(e.Accesses, func(a, b AccountAccess) int {
+func (e *BlockAccessList) Validate(rules params.Rules) 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.Accesses {
- if err := entry.validate(); err != nil {
+ for _, entry := range *e {
+ if err := entry.validate(rules); err != nil {
return err
}
}
@@ -63,56 +95,44 @@ func (e *BlockAccessList) Validate() error {
// Hash computes the keccak256 hash of the access list
func (e *BlockAccessList) Hash() common.Hash {
var enc bytes.Buffer
- err := e.EncodeRLP(&enc)
- if err != nil {
- // errors here are related to BAL values exceeding maximum size defined
- // by the spec. Hard-fail because these cases are not expected to be hit
- // under reasonable conditions.
- panic(err)
+ if err := e.EncodeRLP(&enc); err != nil {
+ // Errors here are related to BAL values exceeding maximum size defined
+ // by the spec. Return empty hash because these cases are not expected
+ // to be hit under reasonable conditions.
+ return common.Hash{}
}
return crypto.Keccak256Hash(enc.Bytes())
}
-// encodeBalance encodes the provided balance into 16-bytes.
-func encodeBalance(val *uint256.Int) [16]byte {
- valBytes := val.Bytes()
- if len(valBytes) > 16 {
- panic("can't encode value that is greater than 16 bytes in size")
- }
- var enc [16]byte
- copy(enc[16-len(valBytes):], valBytes[:])
- return enc
-}
-
// encodingBalanceChange is the encoding format of BalanceChange.
type encodingBalanceChange struct {
- TxIdx uint16 `ssz-size:"2"`
- Balance [16]byte `ssz-size:"16"`
+ TxIdx uint32
+ Balance *uint256.Int
}
// encodingAccountNonce is the encoding format of NonceChange.
type encodingAccountNonce struct {
- TxIdx uint16 `ssz-size:"2"`
- Nonce uint64 `ssz-size:"8"`
+ TxIdx uint32
+ Nonce uint64
}
// encodingStorageWrite is the encoding format of StorageWrites.
type encodingStorageWrite struct {
- TxIdx uint16
- ValueAfter [32]byte `ssz-size:"32"`
+ TxIdx uint32
+ ValueAfter *uint256.Int
}
// encodingStorageWrite is the encoding format of SlotWrites.
type encodingSlotWrites struct {
- Slot [32]byte `ssz-size:"32"`
- Accesses []encodingStorageWrite `ssz-max:"300000"`
+ Slot *uint256.Int
+ Accesses []encodingStorageWrite
}
// validate returns an instance of the encoding-representation slot writes in
// working representation.
func (e *encodingSlotWrites) validate() error {
if slices.IsSortedFunc(e.Accesses, func(a, b encodingStorageWrite) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
}) {
return nil
}
@@ -122,27 +142,27 @@ func (e *encodingSlotWrites) validate() error {
// encodingCodeChange contains the runtime bytecode deployed at an address
// and the transaction index where the deployment took place.
type encodingCodeChange struct {
- TxIndex uint16 `ssz-size:"2"`
- Code []byte `ssz-max:"300000"` // TODO(rjl493456442) shall we put the limit here? The limit will be increased gradually
+ TxIndex uint32
+ Code []byte
}
// AccountAccess is the encoding format of ConstructionAccountAccess.
type AccountAccess struct {
- Address [20]byte `ssz-size:"20"` // 20-byte Ethereum address
- StorageWrites []encodingSlotWrites `ssz-max:"300000"` // Storage changes (slot -> [tx_index -> new_value])
- StorageReads [][32]byte `ssz-max:"300000"` // Read-only storage keys
- BalanceChanges []encodingBalanceChange `ssz-max:"300000"` // Balance changes ([tx_index -> post_balance])
- NonceChanges []encodingAccountNonce `ssz-max:"300000"` // Nonce changes ([tx_index -> new_nonce])
- CodeChanges []encodingCodeChange `ssz-max:"300000"` // Code changes ([tx_index -> new_code])
+ Address [20]byte // 20-byte Ethereum address
+ StorageWrites []encodingSlotWrites // Storage changes (slot -> [tx_index -> new_value])
+ StorageReads []*uint256.Int // Read-only storage keys
+ BalanceChanges []encodingBalanceChange // Balance changes ([tx_index -> post_balance])
+ NonceChanges []encodingAccountNonce // Nonce changes ([tx_index -> new_nonce])
+ CodeChanges []encodingCodeChange // Code changes ([tx_index -> new_code])
}
// 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() error {
+func (e *AccountAccess) validate(rules params.Rules) error {
// Check the storage write slots are sorted in order
if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int {
- return bytes.Compare(a.Slot[:], b.Slot[:])
+ return a.Slot.Cmp(b.Slot)
}) {
return errors.New("storage writes slots not in lexicographic order")
}
@@ -153,36 +173,41 @@ func (e *AccountAccess) validate() error {
}
// Check the storage read slots are sorted in order
- if !slices.IsSortedFunc(e.StorageReads, func(a, b [32]byte) int {
- return bytes.Compare(a[:], b[:])
+ if !slices.IsSortedFunc(e.StorageReads, func(a, b *uint256.Int) int {
+ return a.Cmp(b)
}) {
return errors.New("storage read slots not in lexicographic order")
}
// Check the balance changes are sorted in order
if !slices.IsSortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
}) {
return errors.New("balance changes not in ascending order by tx index")
}
// Check the nonce changes are sorted in order
if !slices.IsSortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
}) {
return errors.New("nonce changes not in ascending order by tx index")
}
// Check the code changes are sorted in order
if !slices.IsSortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int {
- return cmp.Compare[uint16](a.TxIndex, b.TxIndex)
+ return cmp.Compare[uint32](a.TxIndex, b.TxIndex)
}) {
return errors.New("code changes not in ascending order by tx index")
}
for _, change := range e.CodeChanges {
- // TODO(rjl493456442): This check should be fork-aware, since the limit may
- // differ across forks.
- if len(change.Code) > params.MaxCodeSize {
+ var sizeLimit int
+ switch {
+ case rules.IsAmsterdam:
+ sizeLimit = params.MaxCodeSizeAmsterdam
+ default:
+ sizeLimit = params.MaxCodeSize
+ }
+ if len(change.Code) > sizeLimit {
return errors.New("code change contained oversized code")
}
}
@@ -193,16 +218,32 @@ func (e *AccountAccess) validate() error {
func (e *AccountAccess) Copy() AccountAccess {
res := AccountAccess{
Address: e.Address,
- StorageReads: slices.Clone(e.StorageReads),
- BalanceChanges: slices.Clone(e.BalanceChanges),
+ StorageReads: make([]*uint256.Int, 0, len(e.StorageReads)),
+ BalanceChanges: make([]encodingBalanceChange, 0, len(e.BalanceChanges)),
NonceChanges: slices.Clone(e.NonceChanges),
StorageWrites: make([]encodingSlotWrites, 0, len(e.StorageWrites)),
CodeChanges: make([]encodingCodeChange, 0, len(e.CodeChanges)),
}
+ for _, slot := range e.StorageReads {
+ res.StorageReads = append(res.StorageReads, slot.Clone())
+ }
+ for _, change := range e.BalanceChanges {
+ res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{
+ TxIdx: change.TxIdx,
+ Balance: change.Balance.Clone(),
+ })
+ }
for _, storageWrite := range e.StorageWrites {
+ accesses := make([]encodingStorageWrite, 0, len(storageWrite.Accesses))
+ for _, w := range storageWrite.Accesses {
+ accesses = append(accesses, encodingStorageWrite{
+ TxIdx: w.TxIdx,
+ ValueAfter: w.ValueAfter.Clone(),
+ })
+ }
res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{
- Slot: storageWrite.Slot,
- Accesses: slices.Clone(storageWrite.Accesses),
+ Slot: storageWrite.Slot.Clone(),
+ Accesses: accesses,
})
}
for _, codeChange := range e.CodeChanges {
@@ -221,13 +262,13 @@ func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error {
var _ rlp.Encoder = &ConstructionBlockAccessList{}
-// toEncodingObj creates an instance of the ConstructionAccountAccess of the type that is
-// used as input for the encoding.
+// toEncodingObj creates an instance of the ConstructionAccountAccess of the type
+// that is used as input for the encoding.
func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess {
res := AccountAccess{
Address: addr,
StorageWrites: make([]encodingSlotWrites, 0, len(a.StorageWrites)),
- StorageReads: make([][32]byte, 0, len(a.StorageReads)),
+ StorageReads: make([]*uint256.Int, 0, len(a.StorageReads)),
BalanceChanges: make([]encodingBalanceChange, 0, len(a.BalanceChanges)),
NonceChanges: make([]encodingAccountNonce, 0, len(a.NonceChanges)),
CodeChanges: make([]encodingCodeChange, 0, len(a.CodeChange)),
@@ -237,18 +278,19 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
writeSlots := slices.Collect(maps.Keys(a.StorageWrites))
slices.SortFunc(writeSlots, common.Hash.Cmp)
for _, slot := range writeSlots {
- var obj encodingSlotWrites
- obj.Slot = slot
-
+ obj := encodingSlotWrites{
+ Slot: new(uint256.Int).SetBytes(slot[:]),
+ }
slotWrites := a.StorageWrites[slot]
obj.Accesses = make([]encodingStorageWrite, 0, len(slotWrites))
indices := slices.Collect(maps.Keys(slotWrites))
- slices.SortFunc(indices, cmp.Compare[uint16])
+ slices.SortFunc(indices, cmp.Compare[uint32])
for _, index := range indices {
+ val := slotWrites[index]
obj.Accesses = append(obj.Accesses, encodingStorageWrite{
TxIdx: index,
- ValueAfter: slotWrites[index],
+ ValueAfter: new(uint256.Int).SetBytes(val[:]),
})
}
res.StorageWrites = append(res.StorageWrites, obj)
@@ -258,22 +300,22 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
readSlots := slices.Collect(maps.Keys(a.StorageReads))
slices.SortFunc(readSlots, common.Hash.Cmp)
for _, slot := range readSlots {
- res.StorageReads = append(res.StorageReads, slot)
+ res.StorageReads = append(res.StorageReads, new(uint256.Int).SetBytes(slot[:]))
}
// Convert balance changes
balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges))
- slices.SortFunc(balanceIndices, cmp.Compare[uint16])
+ slices.SortFunc(balanceIndices, cmp.Compare[uint32])
for _, idx := range balanceIndices {
res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{
TxIdx: idx,
- Balance: encodeBalance(a.BalanceChanges[idx]),
+ Balance: a.BalanceChanges[idx].Clone(),
})
}
// Convert nonce changes
nonceIndices := slices.Collect(maps.Keys(a.NonceChanges))
- slices.SortFunc(nonceIndices, cmp.Compare[uint16])
+ slices.SortFunc(nonceIndices, cmp.Compare[uint32])
for _, idx := range nonceIndices {
res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{
TxIdx: idx,
@@ -283,11 +325,16 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc
// Convert code change
codeIndices := slices.Collect(maps.Keys(a.CodeChange))
- slices.SortFunc(codeIndices, cmp.Compare[uint16])
+ slices.SortFunc(codeIndices, cmp.Compare[uint32])
for _, idx := range codeIndices {
res.CodeChanges = append(res.CodeChanges, encodingCodeChange{
TxIndex: idx,
- Code: a.CodeChange[idx],
+
+ // TODO(rjl493456442) the contract code is not deep-copied.
+ // In theory the deep-copy is unnecessary, the semantics of
+ // the function should be probably changed that the returned
+ // AccessList is unsafe for modification.
+ Code: a.CodeChange[idx],
})
}
return res
@@ -302,9 +349,9 @@ func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList {
}
slices.SortFunc(addresses, common.Address.Cmp)
- var res BlockAccessList
+ res := make(BlockAccessList, 0, len(addresses))
for _, addr := range addresses {
- res.Accesses = append(res.Accesses, b.Accounts[addr].toEncodingObj(addr))
+ res = append(res, b.Accounts[addr].toEncodingObj(addr))
}
return &res
}
@@ -314,26 +361,25 @@ func (e *BlockAccessList) PrettyPrint() string {
printWithIndent := func(indent int, text string) {
fmt.Fprintf(&res, "%s%s\n", strings.Repeat(" ", indent), text)
}
- for _, accountDiff := range e.Accesses {
+ for _, accountDiff := range *e {
printWithIndent(0, fmt.Sprintf("%x:", accountDiff.Address))
printWithIndent(1, "storage writes:")
for _, sWrite := range accountDiff.StorageWrites {
- printWithIndent(2, fmt.Sprintf("%x:", sWrite.Slot))
+ printWithIndent(2, fmt.Sprintf("%s:", sWrite.Slot.Hex()))
for _, access := range sWrite.Accesses {
- printWithIndent(3, fmt.Sprintf("%d: %x", access.TxIdx, access.ValueAfter))
+ printWithIndent(3, fmt.Sprintf("%d: %s", access.TxIdx, access.ValueAfter.Hex()))
}
}
printWithIndent(1, "storage reads:")
for _, slot := range accountDiff.StorageReads {
- printWithIndent(2, fmt.Sprintf("%x", slot))
+ printWithIndent(2, slot.Hex())
}
printWithIndent(1, "balance changes:")
for _, change := range accountDiff.BalanceChanges {
- balance := new(uint256.Int).SetBytes(change.Balance[:]).String()
- printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, balance))
+ printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, change.Balance))
}
printWithIndent(1, "nonce changes:")
@@ -351,11 +397,9 @@ func (e *BlockAccessList) PrettyPrint() string {
// Copy returns a deep copy of the access list
func (e *BlockAccessList) Copy() *BlockAccessList {
- cpy := &BlockAccessList{
- Accesses: make([]AccountAccess, 0, len(e.Accesses)),
+ cpy := make(BlockAccessList, 0, len(*e))
+ for _, accountAccess := range *e {
+ cpy = append(cpy, accountAccess.Copy())
}
- for _, accountAccess := range e.Accesses {
- cpy.Accesses = append(cpy.Accesses, accountAccess.Copy())
- }
- return cpy
+ return &cpy
}
diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go
index 640035e30e..540987c076 100644
--- a/core/types/bal/bal_encoding_rlp_generated.go
+++ b/core/types/bal/bal_encoding_rlp_generated.go
@@ -3,274 +3,264 @@
package bal
import "github.com/ethereum/go-ethereum/rlp"
+import "github.com/holiman/uint256"
import "io"
-func (obj *BlockAccessList) EncodeRLP(_w io.Writer) error {
+func (obj *AccountAccess) EncodeRLP(_w io.Writer) error {
w := rlp.NewEncoderBuffer(_w)
_tmp0 := w.List()
+ w.WriteBytes(obj.Address[:])
_tmp1 := w.List()
- for _, _tmp2 := range obj.Accesses {
+ for _, _tmp2 := range obj.StorageWrites {
_tmp3 := w.List()
- w.WriteBytes(_tmp2.Address[:])
+ if _tmp2.Slot == nil {
+ w.Write(rlp.EmptyString)
+ } else {
+ w.WriteUint256(_tmp2.Slot)
+ }
_tmp4 := w.List()
- for _, _tmp5 := range _tmp2.StorageWrites {
+ for _, _tmp5 := range _tmp2.Accesses {
_tmp6 := w.List()
- w.WriteBytes(_tmp5.Slot[:])
- _tmp7 := w.List()
- for _, _tmp8 := range _tmp5.Accesses {
- _tmp9 := w.List()
- w.WriteUint64(uint64(_tmp8.TxIdx))
- w.WriteBytes(_tmp8.ValueAfter[:])
- w.ListEnd(_tmp9)
+ w.WriteUint64(uint64(_tmp5.TxIdx))
+ if _tmp5.ValueAfter == nil {
+ w.Write(rlp.EmptyString)
+ } else {
+ w.WriteUint256(_tmp5.ValueAfter)
}
- w.ListEnd(_tmp7)
w.ListEnd(_tmp6)
}
w.ListEnd(_tmp4)
- _tmp10 := w.List()
- for _, _tmp11 := range _tmp2.StorageReads {
- w.WriteBytes(_tmp11[:])
- }
- w.ListEnd(_tmp10)
- _tmp12 := w.List()
- for _, _tmp13 := range _tmp2.BalanceChanges {
- _tmp14 := w.List()
- w.WriteUint64(uint64(_tmp13.TxIdx))
- w.WriteBytes(_tmp13.Balance[:])
- w.ListEnd(_tmp14)
- }
- w.ListEnd(_tmp12)
- _tmp15 := w.List()
- for _, _tmp16 := range _tmp2.NonceChanges {
- _tmp17 := w.List()
- w.WriteUint64(uint64(_tmp16.TxIdx))
- w.WriteUint64(_tmp16.Nonce)
- w.ListEnd(_tmp17)
- }
- w.ListEnd(_tmp15)
- _tmp18 := w.List()
- for _, _tmp19 := range _tmp2.CodeChanges {
- _tmp20 := w.List()
- w.WriteUint64(uint64(_tmp19.TxIndex))
- w.WriteBytes(_tmp19.Code)
- w.ListEnd(_tmp20)
- }
- w.ListEnd(_tmp18)
w.ListEnd(_tmp3)
}
w.ListEnd(_tmp1)
+ _tmp7 := w.List()
+ for _, _tmp8 := range obj.StorageReads {
+ if _tmp8 == nil {
+ w.Write(rlp.EmptyString)
+ } else {
+ w.WriteUint256(_tmp8)
+ }
+ }
+ w.ListEnd(_tmp7)
+ _tmp9 := w.List()
+ for _, _tmp10 := range obj.BalanceChanges {
+ _tmp11 := w.List()
+ w.WriteUint64(uint64(_tmp10.TxIdx))
+ if _tmp10.Balance == nil {
+ w.Write(rlp.EmptyString)
+ } else {
+ w.WriteUint256(_tmp10.Balance)
+ }
+ w.ListEnd(_tmp11)
+ }
+ w.ListEnd(_tmp9)
+ _tmp12 := w.List()
+ for _, _tmp13 := range obj.NonceChanges {
+ _tmp14 := w.List()
+ w.WriteUint64(uint64(_tmp13.TxIdx))
+ w.WriteUint64(_tmp13.Nonce)
+ w.ListEnd(_tmp14)
+ }
+ w.ListEnd(_tmp12)
+ _tmp15 := w.List()
+ for _, _tmp16 := range obj.CodeChanges {
+ _tmp17 := w.List()
+ w.WriteUint64(uint64(_tmp16.TxIndex))
+ w.WriteBytes(_tmp16.Code)
+ w.ListEnd(_tmp17)
+ }
+ w.ListEnd(_tmp15)
w.ListEnd(_tmp0)
return w.Flush()
}
-func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error {
- var _tmp0 BlockAccessList
+func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error {
+ var _tmp0 AccountAccess
{
if _, err := dec.List(); err != nil {
return err
}
- // Accesses:
- var _tmp1 []AccountAccess
+ // Address:
+ var _tmp1 [20]byte
+ if err := dec.ReadBytes(_tmp1[:]); err != nil {
+ return err
+ }
+ _tmp0.Address = _tmp1
+ // StorageWrites:
+ var _tmp2 []encodingSlotWrites
if _, err := dec.List(); err != nil {
return err
}
for dec.MoreDataInList() {
- var _tmp2 AccountAccess
+ var _tmp3 encodingSlotWrites
{
if _, err := dec.List(); err != nil {
return err
}
- // Address:
- var _tmp3 [20]byte
- if err := dec.ReadBytes(_tmp3[:]); err != nil {
+ // Slot:
+ var _tmp4 uint256.Int
+ if err := dec.ReadUint256(&_tmp4); err != nil {
return err
}
- _tmp2.Address = _tmp3
- // StorageWrites:
- var _tmp4 []encodingSlotWrites
+ _tmp3.Slot = &_tmp4
+ // Accesses:
+ var _tmp5 []encodingStorageWrite
if _, err := dec.List(); err != nil {
return err
}
for dec.MoreDataInList() {
- var _tmp5 encodingSlotWrites
- {
- if _, err := dec.List(); err != nil {
- return err
- }
- // Slot:
- var _tmp6 [32]byte
- if err := dec.ReadBytes(_tmp6[:]); err != nil {
- return err
- }
- _tmp5.Slot = _tmp6
- // Accesses:
- var _tmp7 []encodingStorageWrite
- if _, err := dec.List(); err != nil {
- return err
- }
- for dec.MoreDataInList() {
- var _tmp8 encodingStorageWrite
- {
- if _, err := dec.List(); err != nil {
- return err
- }
- // TxIdx:
- _tmp9, err := dec.Uint16()
- if err != nil {
- return err
- }
- _tmp8.TxIdx = _tmp9
- // ValueAfter:
- var _tmp10 [32]byte
- if err := dec.ReadBytes(_tmp10[:]); err != nil {
- return err
- }
- _tmp8.ValueAfter = _tmp10
- if err := dec.ListEnd(); err != nil {
- return err
- }
- }
- _tmp7 = append(_tmp7, _tmp8)
- }
- if err := dec.ListEnd(); err != nil {
- return err
- }
- _tmp5.Accesses = _tmp7
- if err := dec.ListEnd(); err != nil {
- return err
- }
- }
- _tmp4 = append(_tmp4, _tmp5)
- }
- if err := dec.ListEnd(); err != nil {
- return err
- }
- _tmp2.StorageWrites = _tmp4
- // StorageReads:
- var _tmp11 [][32]byte
- if _, err := dec.List(); err != nil {
- return err
- }
- for dec.MoreDataInList() {
- var _tmp12 [32]byte
- if err := dec.ReadBytes(_tmp12[:]); err != nil {
- return err
- }
- _tmp11 = append(_tmp11, _tmp12)
- }
- if err := dec.ListEnd(); err != nil {
- return err
- }
- _tmp2.StorageReads = _tmp11
- // BalanceChanges:
- var _tmp13 []encodingBalanceChange
- if _, err := dec.List(); err != nil {
- return err
- }
- for dec.MoreDataInList() {
- var _tmp14 encodingBalanceChange
+ var _tmp6 encodingStorageWrite
{
if _, err := dec.List(); err != nil {
return err
}
// TxIdx:
- _tmp15, err := dec.Uint16()
+ _tmp7, err := dec.Uint32()
if err != nil {
return err
}
- _tmp14.TxIdx = _tmp15
- // Balance:
- var _tmp16 [16]byte
- if err := dec.ReadBytes(_tmp16[:]); err != nil {
+ _tmp6.TxIdx = _tmp7
+ // ValueAfter:
+ var _tmp8 uint256.Int
+ if err := dec.ReadUint256(&_tmp8); err != nil {
return err
}
- _tmp14.Balance = _tmp16
+ _tmp6.ValueAfter = &_tmp8
if err := dec.ListEnd(); err != nil {
return err
}
}
- _tmp13 = append(_tmp13, _tmp14)
+ _tmp5 = append(_tmp5, _tmp6)
}
if err := dec.ListEnd(); err != nil {
return err
}
- _tmp2.BalanceChanges = _tmp13
- // NonceChanges:
- var _tmp17 []encodingAccountNonce
- if _, err := dec.List(); err != nil {
- return err
- }
- for dec.MoreDataInList() {
- var _tmp18 encodingAccountNonce
- {
- if _, err := dec.List(); err != nil {
- return err
- }
- // TxIdx:
- _tmp19, err := dec.Uint16()
- if err != nil {
- return err
- }
- _tmp18.TxIdx = _tmp19
- // Nonce:
- _tmp20, err := dec.Uint64()
- if err != nil {
- return err
- }
- _tmp18.Nonce = _tmp20
- if err := dec.ListEnd(); err != nil {
- return err
- }
- }
- _tmp17 = append(_tmp17, _tmp18)
- }
- if err := dec.ListEnd(); err != nil {
- return err
- }
- _tmp2.NonceChanges = _tmp17
- // CodeChanges:
- var _tmp21 []encodingCodeChange
- if _, err := dec.List(); err != nil {
- return err
- }
- for dec.MoreDataInList() {
- var _tmp22 encodingCodeChange
- {
- if _, err := dec.List(); err != nil {
- return err
- }
- // TxIndex:
- _tmp23, err := dec.Uint16()
- if err != nil {
- return err
- }
- _tmp22.TxIndex = _tmp23
- // Code:
- _tmp24, err := dec.Bytes()
- if err != nil {
- return err
- }
- _tmp22.Code = _tmp24
- if err := dec.ListEnd(); err != nil {
- return err
- }
- }
- _tmp21 = append(_tmp21, _tmp22)
- }
- if err := dec.ListEnd(); err != nil {
- return err
- }
- _tmp2.CodeChanges = _tmp21
+ _tmp3.Accesses = _tmp5
if err := dec.ListEnd(); err != nil {
return err
}
}
- _tmp1 = append(_tmp1, _tmp2)
+ _tmp2 = append(_tmp2, _tmp3)
}
if err := dec.ListEnd(); err != nil {
return err
}
- _tmp0.Accesses = _tmp1
+ _tmp0.StorageWrites = _tmp2
+ // StorageReads:
+ var _tmp9 []*uint256.Int
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ for dec.MoreDataInList() {
+ var _tmp10 uint256.Int
+ if err := dec.ReadUint256(&_tmp10); err != nil {
+ return err
+ }
+ _tmp9 = append(_tmp9, &_tmp10)
+ }
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ _tmp0.StorageReads = _tmp9
+ // BalanceChanges:
+ var _tmp11 []encodingBalanceChange
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ for dec.MoreDataInList() {
+ var _tmp12 encodingBalanceChange
+ {
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ // TxIdx:
+ _tmp13, err := dec.Uint32()
+ if err != nil {
+ return err
+ }
+ _tmp12.TxIdx = _tmp13
+ // Balance:
+ var _tmp14 uint256.Int
+ if err := dec.ReadUint256(&_tmp14); err != nil {
+ return err
+ }
+ _tmp12.Balance = &_tmp14
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ }
+ _tmp11 = append(_tmp11, _tmp12)
+ }
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ _tmp0.BalanceChanges = _tmp11
+ // NonceChanges:
+ var _tmp15 []encodingAccountNonce
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ for dec.MoreDataInList() {
+ var _tmp16 encodingAccountNonce
+ {
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ // TxIdx:
+ _tmp17, err := dec.Uint32()
+ if err != nil {
+ return err
+ }
+ _tmp16.TxIdx = _tmp17
+ // Nonce:
+ _tmp18, err := dec.Uint64()
+ if err != nil {
+ return err
+ }
+ _tmp16.Nonce = _tmp18
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ }
+ _tmp15 = append(_tmp15, _tmp16)
+ }
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ _tmp0.NonceChanges = _tmp15
+ // CodeChanges:
+ var _tmp19 []encodingCodeChange
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ for dec.MoreDataInList() {
+ var _tmp20 encodingCodeChange
+ {
+ if _, err := dec.List(); err != nil {
+ return err
+ }
+ // TxIndex:
+ _tmp21, err := dec.Uint32()
+ if err != nil {
+ return err
+ }
+ _tmp20.TxIndex = _tmp21
+ // Code:
+ _tmp22, err := dec.Bytes()
+ if err != nil {
+ return err
+ }
+ _tmp20.Code = _tmp22
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ }
+ _tmp19 = append(_tmp19, _tmp20)
+ }
+ if err := dec.ListEnd(); err != nil {
+ return err
+ }
+ _tmp0.CodeChanges = _tmp19
if err := dec.ListEnd(); err != nil {
return err
}
diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go
index 58ba639ff0..32a0292f2e 100644
--- a/core/types/bal/bal_test.go
+++ b/core/types/bal/bal_test.go
@@ -25,22 +25,16 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/testrand"
+ "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)
-func equalBALs(a *BlockAccessList, b *BlockAccessList) bool {
- if !reflect.DeepEqual(a, b) {
- return false
- }
- return true
-}
-
func makeTestConstructionBAL() *ConstructionBlockAccessList {
return &ConstructionBlockAccessList{
map[common.Address]*ConstructionAccountAccess{
common.BytesToAddress([]byte{0xff, 0xff}): {
- StorageWrites: map[common.Hash]map[uint16]common.Hash{
+ StorageWrites: map[common.Hash]map[uint32]common.Hash{
common.BytesToHash([]byte{0x01}): {
1: common.BytesToHash([]byte{1, 2, 3, 4}),
2: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6}),
@@ -52,20 +46,20 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList {
StorageReads: map[common.Hash]struct{}{
common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7}): {},
},
- BalanceChanges: map[uint16]*uint256.Int{
+ BalanceChanges: map[uint32]*uint256.Int{
1: uint256.NewInt(100),
2: uint256.NewInt(500),
},
- NonceChanges: map[uint16]uint64{
+ NonceChanges: map[uint32]uint64{
1: 2,
2: 6,
},
- CodeChange: map[uint16][]byte{
+ CodeChange: map[uint32][]byte{
0: common.Hex2Bytes("deadbeef"),
},
},
common.BytesToAddress([]byte{0xff, 0xff, 0xff}): {
- StorageWrites: map[common.Hash]map[uint16]common.Hash{
+ StorageWrites: map[common.Hash]map[uint32]common.Hash{
common.BytesToHash([]byte{0x01}): {
2: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6}),
3: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
@@ -77,14 +71,14 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList {
StorageReads: map[common.Hash]struct{}{
common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}): {},
},
- BalanceChanges: map[uint16]*uint256.Int{
+ BalanceChanges: map[uint32]*uint256.Int{
2: uint256.NewInt(100),
3: uint256.NewInt(500),
},
- NonceChanges: map[uint16]uint64{
+ NonceChanges: map[uint32]uint64{
1: 2,
},
- CodeChange: map[uint16][]byte{
+ CodeChange: map[uint32][]byte{
0: common.Hex2Bytes("deadbeef"),
},
},
@@ -101,13 +95,13 @@ func TestBALEncoding(t *testing.T) {
t.Fatalf("encoding failed: %v\n", err)
}
var dec BlockAccessList
- if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 10000000)); err != nil {
+ 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() {
t.Fatalf("encoded block hash doesn't match decoded")
}
- if !equalBALs(bal.toEncodingObj(), &dec) {
+ if !reflect.DeepEqual(bal.toEncodingObj(), &dec) {
t.Fatal("decoded BAL doesn't match")
}
}
@@ -115,63 +109,79 @@ func TestBALEncoding(t *testing.T) {
func makeTestAccountAccess(sort bool) AccountAccess {
var (
storageWrites []encodingSlotWrites
- storageReads [][32]byte
+ storageReads []*uint256.Int
balances []encodingBalanceChange
nonces []encodingAccountNonce
+ codes []encodingCodeChange
)
+ randSlot := func() *uint256.Int {
+ return new(uint256.Int).SetBytes(testrand.Bytes(32))
+ }
for i := 0; i < 5; i++ {
slot := encodingSlotWrites{
- Slot: testrand.Hash(),
+ Slot: randSlot(),
}
for j := 0; j < 3; j++ {
slot.Accesses = append(slot.Accesses, encodingStorageWrite{
- TxIdx: uint16(2 * j),
- ValueAfter: testrand.Hash(),
+ TxIdx: uint32(2 * j),
+ ValueAfter: randSlot(),
})
}
if sort {
slices.SortFunc(slot.Accesses, func(a, b encodingStorageWrite) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
})
}
storageWrites = append(storageWrites, slot)
}
if sort {
slices.SortFunc(storageWrites, func(a, b encodingSlotWrites) int {
- return bytes.Compare(a.Slot[:], b.Slot[:])
+ return a.Slot.Cmp(b.Slot)
})
}
for i := 0; i < 5; i++ {
- storageReads = append(storageReads, testrand.Hash())
+ storageReads = append(storageReads, randSlot())
}
if sort {
- slices.SortFunc(storageReads, func(a, b [32]byte) int {
- return bytes.Compare(a[:], b[:])
+ slices.SortFunc(storageReads, func(a, b *uint256.Int) int {
+ return a.Cmp(b)
})
}
for i := 0; i < 5; i++ {
balances = append(balances, encodingBalanceChange{
- TxIdx: uint16(2 * i),
- Balance: [16]byte(testrand.Bytes(16)),
+ TxIdx: uint32(2 * i),
+ Balance: new(uint256.Int).SetBytes(testrand.Bytes(16)),
})
}
if sort {
slices.SortFunc(balances, func(a, b encodingBalanceChange) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
})
}
for i := 0; i < 5; i++ {
nonces = append(nonces, encodingAccountNonce{
- TxIdx: uint16(2 * i),
+ TxIdx: uint32(2 * i),
Nonce: uint64(i + 100),
})
}
if sort {
slices.SortFunc(nonces, func(a, b encodingAccountNonce) int {
- return cmp.Compare[uint16](a.TxIdx, b.TxIdx)
+ return cmp.Compare[uint32](a.TxIdx, b.TxIdx)
+ })
+ }
+
+ for i := 0; i < 5; i++ {
+ codes = append(codes, encodingCodeChange{
+ TxIndex: uint32(2 * i),
+ Code: testrand.Bytes(256),
+ })
+ }
+ if sort {
+ slices.SortFunc(codes, func(a, b encodingCodeChange) int {
+ return cmp.Compare[uint32](a.TxIndex, b.TxIndex)
})
}
@@ -181,26 +191,21 @@ func makeTestAccountAccess(sort bool) AccountAccess {
StorageReads: storageReads,
BalanceChanges: balances,
NonceChanges: nonces,
- CodeChanges: []encodingCodeChange{
- {
- TxIndex: 100,
- Code: testrand.Bytes(256),
- },
- },
+ CodeChanges: codes,
}
}
func makeTestBAL(sort bool) *BlockAccessList {
- list := &BlockAccessList{}
+ list := make(BlockAccessList, 0, 5)
for i := 0; i < 5; i++ {
- list.Accesses = append(list.Accesses, makeTestAccountAccess(sort))
+ list = append(list, makeTestAccountAccess(sort))
}
if sort {
- slices.SortFunc(list.Accesses, func(a, b AccountAccess) int {
+ slices.SortFunc(list, func(a, b AccountAccess) int {
return bytes.Compare(a.Address[:], b.Address[:])
})
}
- return list
+ return &list
}
func TestBlockAccessListCopy(t *testing.T) {
@@ -216,9 +221,9 @@ func TestBlockAccessListCopy(t *testing.T) {
}
// Make sure the mutations on copy won't affect the origin
- for _, aa := range cpyCpy.Accesses {
+ for _, aa := range *cpyCpy {
for i := 0; i < len(aa.StorageReads); i++ {
- aa.StorageReads[i] = [32]byte(testrand.Bytes(32))
+ aa.StorageReads[i] = new(uint256.Int).SetBytes(testrand.Bytes(32))
}
}
if !reflect.DeepEqual(list, cpy) {
@@ -229,7 +234,7 @@ func TestBlockAccessListCopy(t *testing.T) {
func TestBlockAccessListValidation(t *testing.T) {
// Validate the block access list after RLP decoding
enc := makeTestBAL(true)
- if err := enc.Validate(); err != nil {
+ if err := enc.Validate(params.Rules{}); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
var buf bytes.Buffer
@@ -241,14 +246,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(); err != nil {
+ if err := dec.Validate(params.Rules{}); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
// Validate the derived block access list
cBAL := makeTestConstructionBAL()
listB := cBAL.toEncodingObj()
- if err := listB.Validate(); err != nil {
+ if err := listB.Validate(params.Rules{}); err != nil {
t.Fatalf("Unexpected validation error: %v", err)
}
}
diff --git a/core/types/hashes.go b/core/types/hashes.go
index 22f1f946dc..db8912a66f 100644
--- a/core/types/hashes.go
+++ b/core/types/hashes.go
@@ -43,9 +43,6 @@ var (
// EmptyRequestsHash is the known hash of an empty request set, sha256("").
EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
- // EmptyVerkleHash is the known hash of an empty verkle trie.
- EmptyVerkleHash = common.Hash{}
-
// EmptyBinaryHash is the known hash of an empty binary trie.
EmptyBinaryHash = common.Hash{}
)
diff --git a/core/types/tx_legacy.go b/core/types/tx_legacy.go
index 49f0a98809..eca9e210af 100644
--- a/core/types/tx_legacy.go
+++ b/core/types/tx_legacy.go
@@ -121,7 +121,7 @@ func (tx *LegacyTx) encode(*bytes.Buffer) error {
}
func (tx *LegacyTx) decode([]byte) error {
- panic("decode called on LegacyTx)")
+ panic("decode called on LegacyTx")
}
// OBS: This is the post-EIP155 hash, the pre-EIP155 does not contain a chainID.
diff --git a/core/vm/contract.go b/core/vm/contract.go
index 3b5695e21a..45c879c80f 100644
--- a/core/vm/contract.go
+++ b/core/vm/contract.go
@@ -42,12 +42,12 @@ type Contract struct {
IsDeployment bool
IsSystemCall bool
- Gas GasCosts
+ Gas GasBudget
value *uint256.Int
}
// NewContract returns a new contract environment for the execution of EVM.
-func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) *Contract {
+func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas GasBudget, jumpDests JumpDestCache) *Contract {
// Initialize the jump analysis cache if it's nil, mostly for tests
if jumpDests == nil {
jumpDests = newMapJumpDests()
@@ -56,7 +56,7 @@ func NewContract(caller common.Address, address common.Address, value *uint256.I
caller: caller,
address: address,
jumpDests: jumpDests,
- Gas: GasCosts{RegularGas: gas},
+ Gas: gas,
value: value,
}
}
@@ -126,26 +126,26 @@ func (c *Contract) Caller() common.Address {
}
// UseGas attempts the use gas and subtracts it and returns true on success
-func (c *Contract) UseGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) {
- if c.Gas.RegularGas < gas {
+func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) {
+ prior, ok := c.Gas.Charge(cost)
+ if !ok {
return false
}
- if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
- logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas-gas, reason)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
- c.Gas.RegularGas -= gas
return true
}
-// RefundGas refunds gas to the contract
-func (c *Contract) RefundGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
- if gas == 0 {
+// RefundGas refunds the leftover gas budget back to the contract.
+func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
+ prior, changed := c.Gas.Refund(refund)
+ if !changed {
return
}
- if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
- logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas+gas, reason)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
- c.Gas.RegularGas += gas
}
// Address returns the contracts address
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index 010f477337..71cfdbc527 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -213,7 +213,7 @@ func init() {
func activePrecompiledContracts(rules params.Rules) PrecompiledContracts {
switch {
- case rules.IsVerkle:
+ case rules.IsUBT:
return PrecompiledContractsVerkle
case rules.IsOsaka:
return PrecompiledContractsOsaka
@@ -260,25 +260,25 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
// RunPrecompiledContract runs and evaluates the output of a precompiled contract.
// It returns
// - the returned bytes,
-// - the _remaining_ gas,
+// - the remaining gas budget,
// - any error that occurred
-func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) {
+func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) {
gasCost := p.RequiredGas(input)
- if suppliedGas < gasCost {
- return nil, 0, ErrOutOfGas
+ prior, ok := gas.Charge(GasCosts{RegularGas: gasCost})
+ if !ok {
+ gas.Exhaust()
+ return nil, gas, ErrOutOfGas
}
- if logger != nil && logger.OnGasChange != nil {
- logger.OnGasChange(suppliedGas, suppliedGas-gasCost, tracing.GasChangeCallPrecompiledContract)
+ if logger.HasGasHook() {
+ logger.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeCallPrecompiledContract)
}
- suppliedGas -= gasCost
-
// Touch the precompile for block-level accessList recording once Amsterdam
// fork is activated.
- if stateDB != nil {
- stateDB.Exist(address)
+ if rules.IsAmsterdam {
+ stateDB.Touch(address)
}
output, err := p.Run(input)
- return output, suppliedGas, err
+ return output, gas, err
}
// ecrecover implemented as a native contract.
diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go
index 34ed1d87fe..988cdb91f2 100644
--- a/core/vm/contracts_fuzz_test.go
+++ b/core/vm/contracts_fuzz_test.go
@@ -20,6 +20,7 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/params"
)
func FuzzPrecompiledContracts(f *testing.F) {
@@ -36,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) {
return
}
inWant := string(input)
- RunPrecompiledContract(nil, p, a, input, gas, nil)
+ RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{})
if inHave := string(input); inWant != inHave {
t.Errorf("Precompiled %v modified input data", a)
}
diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go
index b647dcc62b..e7841c8552 100644
--- a/core/vm/contracts_test.go
+++ b/core/vm/contracts_test.go
@@ -25,6 +25,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/params"
)
// precompiledTest defines the input/output pairs for precompiled contract tests.
@@ -99,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil); err != nil {
+ if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil {
t.Error(err)
} else if common.Bytes2Hex(res) != test.Expected {
t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res))
@@ -121,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) {
gas := test.Gas - 1
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil)
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
if err.Error() != "out of gas" {
t.Errorf("Expected error [out of gas], got [%v]", err)
}
@@ -138,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(test.Name, func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil)
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
if err.Error() != test.ExpectedError {
t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err)
}
@@ -169,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) {
start := time.Now()
for bench.Loop() {
copy(data, in)
- res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, reqGas, nil)
+ res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{})
}
elapsed := uint64(time.Since(start))
if elapsed < 1 {
diff --git a/core/vm/eip7610.go b/core/vm/eip7610.go
new file mode 100644
index 0000000000..883f4502b5
--- /dev/null
+++ b/core/vm/eip7610.go
@@ -0,0 +1,98 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package vm
+
+import (
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// eip7610Accounts lists the addresses eligible for contract deployment
+// rejection under EIP-7610, keyed by chain ID. Only networks that adopted
+// EIP-158 after genesis need an entry; all others have no pre-existing
+// address collisions to guard against.
+var eip7610Accounts = map[uint64][]common.Address{
+ params.MainnetChainConfig.ChainID.Uint64(): {
+ common.HexToAddress("0x02820E4bEE488C40f7455fDCa53125565148708F"),
+ common.HexToAddress("0x14725085d004f1b10Ee07234A4ab28c5Ad2a7b9E"),
+ common.HexToAddress("0x19272418753B90D9a3E3Efc8430b1612c55fcB3A"),
+ common.HexToAddress("0x2c081Ed1949D7Dd9447F9d96e509befE576D4461"),
+ common.HexToAddress("0x3311c08066580cb906a7287b6786E504C2EBD09f"),
+ common.HexToAddress("0x361d7a60b43587c7f6bbA4f9fD9642747F65210A"),
+ common.HexToAddress("0x40490C9c468622d5c89646D6F3097F8Eaf80c411"),
+ common.HexToAddress("0x4d149EB99BDEEFC1f858f8fd22289C6beAE99f2c"),
+ common.HexToAddress("0x5071cb62aA170b7f66b26cae8004d90E6078Bb1E"),
+ common.HexToAddress("0x50b1497068bAE652Df3562EB8Ea7677ff84477FA"),
+ common.HexToAddress("0x5983C6aC846DcF85fbBC4303F43eb91C379F79ae"),
+ common.HexToAddress("0x59EC0410867828E3b8c23Dd8A29d9796ef523b17"),
+ common.HexToAddress("0x5cC182faBFb81A056B6080d4200BC5150673D06f"),
+ common.HexToAddress("0x6f156dbf8Ed30e53F7C9Df73144E69f65cBB7E94"),
+ common.HexToAddress("0x7D6ae067De8d44Ae1A08750e7D626D61A623C44A"),
+ common.HexToAddress("0x8398fF6c618e9515468c1c4b198d53666CBe8462"),
+ common.HexToAddress("0xA21B22389bfC1cd6Bc7BA19A4Fc96aDC3D0FE074"),
+ common.HexToAddress("0xaDD92e0650457C5Db0c4c08cbf7cA580175d33d2"),
+ common.HexToAddress("0xAE3703584494Ade958AD27EC2d289b7a67c19E90"),
+ common.HexToAddress("0xb619f45637C39Ca49A41ac64c11637A0A194455E"),
+ common.HexToAddress("0xD8253352f6044cFE55bcC0748C3FA37b7dF81F98"),
+ common.HexToAddress("0xDB7C577B93Baeb56dAB50aF4D6f86F99A06B96a2"),
+ common.HexToAddress("0xdE425ad4B8d2d9E0E12F65CBcD6D55F447B44083"),
+ common.HexToAddress("0xe62dc49C92fA799033644d2A9aFD7e3BAbE5A80a"),
+ common.HexToAddress("0xF468BcBC4a0BFDB06336E773382C5202E674db71"),
+ common.HexToAddress("0xF4a835ec1364809003dE3925685F24cD360bdffe"),
+ common.HexToAddress("0xFc4465F84B29a1F8794Dc753F41BeF1F4b025ED2"),
+ common.HexToAddress("0xfeE7707fa4b8C0A923A0E40399Db3e7Ce26069C6"),
+ },
+}
+
+// eip7610AccountSets is the membership-lookup form of eip7610Accounts,
+// built once at init for O(1) containment checks.
+var eip7610AccountSets = func() map[uint64]map[common.Address]struct{} {
+ sets := make(map[uint64]map[common.Address]struct{}, len(eip7610Accounts))
+ for chainID, addrs := range eip7610Accounts {
+ set := make(map[common.Address]struct{}, len(addrs))
+ for _, a := range addrs {
+ set[a] = struct{}{}
+ }
+ sets[chainID] = set
+ }
+ return sets
+}()
+
+// isEIP7610RejectedAccount reports whether the account identified by the
+// address is eligible for contract deployment rejection due to having
+// non-empty storage.
+//
+// Note that, historically, there has been no case where a contract deployment
+// targets an already existing account in Ethereum. This situation would only
+// occur in the event of an address collision, which is extremely unlikely.
+//
+// This check is skipped for blocks prior to EIP-158, serving as a safeguard
+// against potential address collisions in the future. Chains that are not
+// registered in eip7610Accounts are assumed to have no rejected accounts,
+// and false is returned for them.
+func isEIP7610RejectedAccount(chainID *big.Int, addr common.Address, isEIP158 bool) bool {
+ // Short circuit for blocks prior to EIP-158.
+ if !isEIP158 {
+ return false
+ }
+ // Unknown chains fall through as a nil set; the second lookup then
+ // returns the zero value (false), treating the chain as empty.
+ _, exist := eip7610AccountSets[chainID.Uint64()][addr]
+ return exist
+}
diff --git a/core/vm/eip7610_test.go b/core/vm/eip7610_test.go
new file mode 100644
index 0000000000..f881020c5c
--- /dev/null
+++ b/core/vm/eip7610_test.go
@@ -0,0 +1,62 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package vm
+
+import (
+ "fmt"
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+func Example_mainnetEIP7610Accounts() {
+ list := slices.Clone(eip7610Accounts[params.MainnetChainConfig.ChainID.Uint64()])
+ slices.SortFunc(list, common.Address.Cmp)
+ for _, addr := range list {
+ fmt.Println(addr.Hex())
+ }
+ // Output:
+ // 0x02820E4bEE488C40f7455fDCa53125565148708F
+ // 0x14725085d004f1b10Ee07234A4ab28c5Ad2a7b9E
+ // 0x19272418753B90D9a3E3Efc8430b1612c55fcB3A
+ // 0x2c081Ed1949D7Dd9447F9d96e509befE576D4461
+ // 0x3311c08066580cb906a7287b6786E504C2EBD09f
+ // 0x361d7a60b43587c7f6bbA4f9fD9642747F65210A
+ // 0x40490C9c468622d5c89646D6F3097F8Eaf80c411
+ // 0x4d149EB99BDEEFC1f858f8fd22289C6beAE99f2c
+ // 0x5071cb62aA170b7f66b26cae8004d90E6078Bb1E
+ // 0x50b1497068bAE652Df3562EB8Ea7677ff84477FA
+ // 0x5983C6aC846DcF85fbBC4303F43eb91C379F79ae
+ // 0x59EC0410867828E3b8c23Dd8A29d9796ef523b17
+ // 0x5cC182faBFb81A056B6080d4200BC5150673D06f
+ // 0x6f156dbf8Ed30e53F7C9Df73144E69f65cBB7E94
+ // 0x7D6ae067De8d44Ae1A08750e7D626D61A623C44A
+ // 0x8398fF6c618e9515468c1c4b198d53666CBe8462
+ // 0xA21B22389bfC1cd6Bc7BA19A4Fc96aDC3D0FE074
+ // 0xaDD92e0650457C5Db0c4c08cbf7cA580175d33d2
+ // 0xAE3703584494Ade958AD27EC2d289b7a67c19E90
+ // 0xb619f45637C39Ca49A41ac64c11637A0A194455E
+ // 0xD8253352f6044cFE55bcC0748C3FA37b7dF81F98
+ // 0xDB7C577B93Baeb56dAB50aF4D6f86F99A06B96a2
+ // 0xdE425ad4B8d2d9E0E12F65CBcD6D55F447B44083
+ // 0xe62dc49C92fA799033644d2A9aFD7e3BAbE5A80a
+ // 0xF468BcBC4a0BFDB06336E773382C5202E674db71
+ // 0xF4a835ec1364809003dE3925685F24cD360bdffe
+ // 0xFc4465F84B29a1F8794Dc753F41BeF1F4b025ED2
+ // 0xfeE7707fa4b8C0A923A0E40399Db3e7Ce26069C6
+}
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 8f4ca3ae41..33af8fd4fd 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -24,7 +24,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/params"
- "github.com/holiman/uint256"
)
var activators = map[int]func(*JumpTable){
@@ -92,8 +91,7 @@ func enable1884(jt *JumpTable) {
}
func opSelfBalance(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- balance := evm.StateDB.GetBalance(scope.Contract.Address())
- scope.Stack.push(balance)
+ scope.Stack.get().Set(evm.StateDB.GetBalance(scope.Contract.Address()))
return nil, nil
}
@@ -111,8 +109,7 @@ func enable1344(jt *JumpTable) {
// opChainID implements CHAINID opcode
func opChainID(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- chainId, _ := uint256.FromBig(evm.chainConfig.ChainID)
- scope.Stack.push(chainId)
+ scope.Stack.get().SetFromBig(evm.chainConfig.ChainID)
return nil, nil
}
@@ -222,8 +219,7 @@ func opTstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// opBaseFee implements BASEFEE opcode
func opBaseFee(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- baseFee, _ := uint256.FromBig(evm.Context.BaseFee)
- scope.Stack.push(baseFee)
+ scope.Stack.get().SetFromBig(evm.Context.BaseFee)
return nil, nil
}
@@ -240,7 +236,7 @@ func enable3855(jt *JumpTable) {
// opPush0 implements the PUSH0 opcode
func opPush0(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int))
+ scope.Stack.get().Clear()
return nil, nil
}
@@ -291,8 +287,7 @@ func opBlobHash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// opBlobBaseFee implements BLOBBASEFEE opcode
func opBlobBaseFee(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- blobBaseFee, _ := uint256.FromBig(evm.Context.BlobBaseFee)
- scope.Stack.push(blobBaseFee)
+ scope.Stack.get().SetFromBig(evm.Context.BlobBaseFee)
return nil, nil
}
@@ -382,7 +377,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
code := evm.StateDB.GetCode(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -397,24 +392,24 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
codeLen = uint64(len(scope.Contract.Code))
- integer = new(uint256.Int)
+ elem = scope.Stack.get()
)
*pc += 1
if *pc < codeLen {
- scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc])))
+ elem.SetUint64(uint64(scope.Contract.Code[*pc]))
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall && *pc%31 == 0 {
// touch next chunk if PUSH1 is at the boundary. if so, *pc has
// advanced past this boundary.
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.UseGas(GasCosts{RegularGas: wanted}, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
}
} else {
- scope.Stack.push(integer.Clear())
+ elem.Clear()
}
return nil, nil
}
@@ -426,17 +421,16 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc {
start = min(codeLen, int(*pc+1))
end = min(codeLen, start+pushByteSize)
)
- scope.Stack.push(new(uint256.Int).SetBytes(
+ scope.Stack.get().SetBytes(
common.RightPadBytes(
scope.Contract.Code[start:end],
pushByteSize,
- )),
- )
+ ))
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall {
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -583,7 +577,7 @@ func enable7702(jt *JumpTable) {
// opSlotNum enables the SLOTNUM opcode
func opSlotNum(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(uint256.NewInt(evm.Context.SlotNum))
+ scope.Stack.get().SetUint64(evm.Context.SlotNum)
return nil, nil
}
diff --git a/core/vm/errors.go b/core/vm/errors.go
index e33c9fcb85..b6235d44a6 100644
--- a/core/vm/errors.go
+++ b/core/vm/errors.go
@@ -76,10 +76,16 @@ func (e ErrStackOverflow) Unwrap() error {
// ErrInvalidOpCode wraps an evm error when an invalid opcode is encountered.
type ErrInvalidOpCode struct {
- opcode OpCode
+ opcode OpCode
+ operand *byte
}
-func (e *ErrInvalidOpCode) Error() string { return fmt.Sprintf("invalid opcode: %s", e.opcode) }
+func (e *ErrInvalidOpCode) Error() string {
+ if e.operand != nil {
+ return fmt.Sprintf("invalid opcode: %s (operand: 0x%02x)", e.opcode, *e.operand)
+ }
+ return fmt.Sprintf("invalid opcode: %s", e.opcode)
+}
// rpcError is the same interface as the one defined in rpc/errors.go
// but we do not want to depend on rpc package here so we redefine it.
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 4df2627486..9fe6faa3a2 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -127,6 +127,8 @@ type EVM struct {
readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse
+
+ arena *stackArena
}
// NewEVM constructs an EVM instance with the supplied block context, state
@@ -141,6 +143,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
jumpDests: newMapJumpDests(),
+ arena: newArena(),
}
evm.precompiles = activePrecompiledContracts(evm.chainRules)
@@ -149,7 +152,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
evm.table = &amsterdamInstructionSet
case evm.chainRules.IsOsaka:
evm.table = &osakaInstructionSet
- case evm.chainRules.IsVerkle:
+ case evm.chainRules.IsUBT:
// TODO replace with proper instruction set when fork is specified
evm.table = &verkleInstructionSet
case evm.chainRules.IsPrague:
@@ -223,6 +226,12 @@ func (evm *EVM) Cancel() {
evm.abort.Store(true)
}
+// Release returns some memory allocated by the EVM, should be called after the EVM was used
+// for the last time. Not necessary, but an improvement.
+func (evm *EVM) Release() {
+ returnStack(evm.arena)
+}
+
// Cancelled returns true if Cancel has been called
func (evm *EVM) Cancelled() bool {
return evm.abort.Load()
@@ -236,13 +245,13 @@ func isSystemCall(caller common.Address) bool {
// parameters. It also handles any necessary value transfer required and takse
// the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer.
-func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
+func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
// Capture the tracer start/end events in debug mode
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())
+ evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig())
defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
- }(gas)
+ evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
+ }(gas.RegularGas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -265,12 +274,12 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// list in write mode. If there is enough gas paying for the addition of the code
// hash leaf to the access list, then account creation will proceed unimpaired.
// Thus, only pay for the creation of the code hash leaf here.
- wgas := evm.AccessEvents.CodeHashGas(addr, true, gas, false)
- if gas < wgas {
+ wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
+ if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok {
evm.StateDB.RevertToSnapshot(snapshot)
- return nil, 0, ErrOutOfGas
+ gas.Exhaust()
+ return nil, gas, ErrOutOfGas
}
- gas -= wgas
}
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
@@ -287,11 +296,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
if isPrecompile {
- var stateDB StateDB
- if evm.chainRules.IsAmsterdam {
- stateDB = evm.StateDB
- }
- ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer)
+ ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
code := evm.resolveCode(addr)
@@ -303,7 +308,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
contract.IsSystemCall = isSystemCall(caller)
contract.SetCallCode(evm.resolveCodeHash(addr), code)
ret, err = evm.Run(contract, input, false)
- gas = contract.Gas.RegularGas
+ gas = contract.Gas
}
}
// When an error was returned by the EVM or when setting the creation code
@@ -312,10 +317,10 @@ 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, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
- gas = 0
+ gas.Exhaust()
}
// TODO: consider clearing up unused snapshots:
//} else {
@@ -331,13 +336,13 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
//
// CallCode differs from Call in the sense that it executes the given address'
// code with the caller as context.
-func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
+func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig())
+ evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig())
defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
- }(gas)
+ evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
+ }(gas.RegularGas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -354,26 +359,22 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- var stateDB StateDB
- if evm.chainRules.IsAmsterdam {
- stateDB = evm.StateDB
- }
- ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer)
+ ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
- gas = contract.Gas.RegularGas
+ gas = contract.Gas
}
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, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
- gas = 0
+ gas.Exhaust()
}
}
return ret, gas, err
@@ -384,14 +385,14 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
//
// DelegateCall differs from CallCode in the sense that it executes the given address'
// code with the caller as context and the caller is set to the caller of the caller.
-func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
+func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
// DELEGATECALL inherits value from parent call
- evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig())
+ evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig())
defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
- }(gas)
+ evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
+ }(gas.RegularGas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -401,11 +402,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- var stateDB StateDB
- if evm.chainRules.IsAmsterdam {
- stateDB = evm.StateDB
- }
- ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer)
+ ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
// Initialise a new contract and make initialise the delegate values
//
@@ -413,15 +410,15 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
contract := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
- gas = contract.Gas.RegularGas
+ gas = contract.Gas
}
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, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
- gas = 0
+ gas.Exhaust()
}
}
return ret, gas, err
@@ -431,13 +428,13 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
// as parameters while disallowing any modifications to the state during the call.
// Opcodes that attempt to perform such modifications will result in exceptions
// instead of performing the modifications.
-func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {
+func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, leftOverGas GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil)
+ evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil)
defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
- }(gas)
+ evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
+ }(gas.RegularGas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -457,11 +454,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount)
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- var stateDB StateDB
- if evm.chainRules.IsAmsterdam {
- stateDB = evm.StateDB
- }
- ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer)
+ ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
@@ -472,28 +465,27 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in Homestead this also counts for code storage gas errors.
ret, err = evm.Run(contract, input, true)
- gas = contract.Gas.RegularGas
+ gas = contract.Gas
}
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, 0, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
-
- gas = 0
+ gas.Exhaust()
}
}
return ret, gas, err
}
// create creates a new contract using code as deployment code.
-func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas uint64, err error) {
+func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasBudget, err error) {
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig())
+ evm.captureBegin(evm.depth, typ, caller, address, code, gas.RegularGas, value.ToBig())
defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
- }(gas)
+ evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
+ }(gas.RegularGas)
}
// Depth check execution. Fail if we're trying to execute above the
// limit.
@@ -511,14 +503,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
- statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas)
- if statelessGas > gas {
- return nil, common.Address{}, 0, ErrOutOfGas
+ statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
+ prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
+ if !ok {
+ gas.Exhaust()
+ return nil, common.Address{}, gas, ErrOutOfGas
}
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas, gas-statelessGas, tracing.GasChangeWitnessContractCollisionCheck)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
}
- gas = gas - statelessGas
}
// We add this to the access list _before_ taking a snapshot. Even if the
@@ -532,14 +525,14 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
// - the code is non-empty
// - the storage is non-empty
contractHash := evm.StateDB.GetCodeHash(address)
- storageRoot := evm.StateDB.GetStorageRoot(address)
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
- (storageRoot != (common.Hash{}) && storageRoot != types.EmptyRootHash) { // non-empty storage
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution)
+ isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
- return nil, common.Address{}, 0, ErrContractAddressCollision
+ gas.Exhaust()
+ return nil, common.Address{}, gas, ErrContractAddressCollision
}
// Create a new account on the state only if the object was not present.
// It might be possible the contract code is deployed to a pre-existent
@@ -559,14 +552,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
}
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
- consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas)
+ consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted {
- return nil, common.Address{}, 0, ErrOutOfGas
+ gas.Exhaust()
+ return nil, common.Address{}, gas, ErrOutOfGas
}
- if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
- evm.Config.Tracer.OnGasChange(gas, gas-consumed, tracing.GasChangeWitnessContractInit)
+ prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractInit)
}
- gas = gas - consumed
}
evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)
@@ -583,10 +577,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
- contract.UseGas(contract.Gas.RegularGas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
+ contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
}
}
- return ret, address, contract.Gas.RegularGas, err
+ return ret, address, contract.Gas, err
}
// initNewContract runs a new contract's creation code, performs checks on the
@@ -609,12 +603,12 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
if !evm.chainRules.IsEIP4762 {
createDataGas := uint64(len(ret)) * params.CreateDataGas
- if !contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
}
} else {
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
- contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if len(ret) > 0 && (consumed < wanted) {
return ret, ErrCodeStoreOutOfGas
}
@@ -627,7 +621,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
}
// Create creates a new contract using code as deployment code.
-func (evm *EVM) Create(caller common.Address, code []byte, gas uint64, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
+func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE)
}
@@ -636,7 +630,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas uint64, value *ui
//
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
-func (evm *EVM) Create2(caller common.Address, code []byte, gas uint64, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
+func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)
@@ -679,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 {
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index b3259b2ec7..046311f9cc 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -71,7 +71,7 @@ func memoryCopierGas(stackpos int) gasFunc {
return GasCosts{}, err
}
// And gas for copying data, charged per word at param.CopyGas
- words, overflow := stack.Back(stackpos).Uint64WithOverflow()
+ words, overflow := stack.back(stackpos).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -100,7 +100,7 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
return GasCosts{}, ErrWriteProtection
}
var (
- y, x = stack.Back(1), stack.Back(0)
+ y, x = stack.back(1), stack.back(0)
current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), x.Bytes32())
)
// The legacy gas metering only takes into consideration the current state
@@ -192,7 +192,7 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
// Gas sentry honoured, do the actual gas calculation based on the stored value
var (
- y, x = stack.Back(1), stack.Back(0)
+ y, x = stack.back(1), stack.back(0)
current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), x.Bytes32())
)
value := common.Hash(y.Bytes32())
@@ -228,7 +228,7 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
func makeGasLog(n uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
- requestedSize, overflow := stack.Back(1).Uint64WithOverflow()
+ requestedSize, overflow := stack.back(1).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -261,7 +261,7 @@ func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
if err != nil {
return GasCosts{}, err
}
- wordGas, overflow := stack.Back(1).Uint64WithOverflow()
+ wordGas, overflow := stack.back(1).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -299,7 +299,7 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS
if err != nil {
return GasCosts{}, err
}
- wordGas, overflow := stack.Back(2).Uint64WithOverflow()
+ wordGas, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -317,7 +317,7 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
if err != nil {
return GasCosts{}, err
}
- size, overflow := stack.Back(2).Uint64WithOverflow()
+ size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -336,7 +336,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
if err != nil {
return GasCosts{}, err
}
- size, overflow := stack.Back(2).Uint64WithOverflow()
+ size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -352,7 +352,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
}
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
- expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8)
+ expByteLen := uint64((stack.back(1).BitLen() + 7) / 8)
var (
gas = expByteLen * params.ExpByteFrontier // no overflow check required. Max is 256 * ExpByte gas
@@ -365,7 +365,7 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
}
func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
- expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8)
+ expByteLen := uint64((stack.back(1).BitLen() + 7) / 8)
var (
gas = expByteLen * params.ExpByteEIP158 // no overflow check required. Max is 256 * ExpByte gas
@@ -390,7 +390,7 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
if err != nil {
return GasCosts{}, err
}
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.Back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0))
if err != nil {
return GasCosts{}, err
}
@@ -405,8 +405,8 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas uint64
- transfersValue = !stack.Back(2).IsZero()
- address = common.Address(stack.Back(1).Bytes20())
+ transfersValue = !stack.back(2).IsZero()
+ address = common.Address(stack.back(1).Bytes20())
)
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
@@ -453,7 +453,7 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor
gas uint64
overflow bool
)
- if stack.Back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 {
+ if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
}
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
@@ -487,7 +487,7 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
// EIP150 homestead gas reprice fork:
if evm.chainRules.IsEIP150 {
gas = params.SelfdestructGasEIP150
- var address = common.Address(stack.Back(0).Bytes20())
+ var address = common.Address(stack.back(0).Bytes20())
if evm.chainRules.IsEIP158 {
// if empty and transfers value
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index e9e56038dd..16ce651a7d 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) {
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
-
- _, gas, err := evm.Call(common.Address{}, address, nil, tt.gaspool, new(uint256.Int))
+ initialGas := NewGasBudget(tt.gaspool)
+ _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
if !errors.Is(err, tt.failure) {
t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure)
}
- if used := tt.gaspool - gas; used != tt.used {
+ if used := leftOver.Used(initialGas); used != tt.used {
t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
}
if refund := evm.StateDB.GetRefund(); refund != tt.refund {
@@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) {
}
evm := NewEVM(vmctx, statedb, chainConfig, config)
- var startGas = uint64(testGas)
- ret, gas, err := evm.Call(common.Address{}, address, nil, startGas, new(uint256.Int))
+ initialGas := NewGasBudget(uint64(testGas))
+ ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
if err != nil {
return false
}
- gasUsed = startGas - gas
+ gasUsed = leftOver.Used(initialGas)
if len(ret) != 32 {
t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret))
}
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index ba6746758b..ed938ae41f 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -16,10 +16,15 @@
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.
+// multidimensional metering paradigm. It represents the cost
+// charged by an individual operation.
type GasCosts struct {
RegularGas uint64
StateGas uint64
@@ -34,3 +39,68 @@ func (g GasCosts) Sum() uint64 {
func (g GasCosts) String() string {
return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
}
+
+// GasBudget denotes a vector of remaining gas allowances available
+// for EVM execution in the multidimensional metering paradigm.
+// Unlike GasCosts which represents the price of an operation,
+// GasBudget tracks how much gas is left to spend.
+type GasBudget struct {
+ RegularGas uint64 // The leftover gas for execution and state gas usage
+ StateGas uint64 // The state gas reservoir
+}
+
+// NewGasBudget creates a GasBudget with the given initial regular gas allowance.
+func NewGasBudget(gas uint64) GasBudget {
+ return GasBudget{RegularGas: gas}
+}
+
+// Used returns the amount of regular gas consumed so far.
+func (g GasBudget) Used(initial GasBudget) uint64 {
+ return initial.RegularGas - g.RegularGas
+}
+
+// Exhaust sets all remaining gas to zero, preserving the initial amount
+// for usage tracking.
+func (g *GasBudget) Exhaust() {
+ g.RegularGas = 0
+ g.StateGas = 0
+}
+
+func (g *GasBudget) Copy() GasBudget {
+ return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas}
+}
+
+// String returns a visual representation of the gas budget vector.
+func (g GasBudget) String() string {
+ return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
+}
+
+// CanAfford reports whether the budget has sufficient gas to cover the cost.
+func (g GasBudget) CanAfford(cost GasCosts) bool {
+ return g.RegularGas >= cost.RegularGas
+}
+
+// Charge deducts the given gas cost from the budget. It returns the
+// pre-charge budget and false if the budget does not have sufficient
+// gas to cover the cost.
+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 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.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/instructions.go b/core/vm/instructions.go
index 74400732ac..4b05092cc7 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -24,7 +24,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
- "github.com/holiman/uint256"
)
func opAdd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
@@ -244,7 +243,7 @@ func opKeccak256(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opAddress(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Address().Bytes()))
+ scope.Stack.get().SetBytes(scope.Contract.Address().Bytes())
return nil, nil
}
@@ -256,17 +255,17 @@ func opBalance(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opOrigin(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetBytes(evm.Origin.Bytes()))
+ scope.Stack.get().SetBytes(evm.Origin.Bytes())
return nil, nil
}
func opCaller(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Caller().Bytes()))
+ scope.Stack.get().SetBytes(scope.Contract.Caller().Bytes())
return nil, nil
}
func opCallValue(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(scope.Contract.value)
+ scope.Stack.get().Set(scope.Contract.value)
return nil, nil
}
@@ -282,7 +281,7 @@ func opCallDataLoad(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCallDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(scope.Contract.Input))))
+ scope.Stack.get().SetUint64(uint64(len(scope.Contract.Input)))
return nil, nil
}
@@ -305,7 +304,7 @@ func opCallDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opReturnDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(evm.returnData))))
+ scope.Stack.get().SetUint64(uint64(len(evm.returnData)))
return nil, nil
}
@@ -338,7 +337,7 @@ func opExtCodeSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCodeSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(scope.Contract.Code))))
+ scope.Stack.get().SetUint64(uint64(len(scope.Contract.Code)))
return nil, nil
}
@@ -416,7 +415,7 @@ func opExtCodeHash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opGasprice(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(evm.GasPrice.Clone())
+ scope.Stack.get().Set(evm.GasPrice)
return nil, nil
}
@@ -451,35 +450,32 @@ func opBlockhash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCoinbase(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetBytes(evm.Context.Coinbase.Bytes()))
+ scope.Stack.get().SetBytes(evm.Context.Coinbase.Bytes())
return nil, nil
}
func opTimestamp(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(evm.Context.Time))
+ scope.Stack.get().SetUint64(evm.Context.Time)
return nil, nil
}
func opNumber(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- v, _ := uint256.FromBig(evm.Context.BlockNumber)
- scope.Stack.push(v)
+ scope.Stack.get().SetFromBig(evm.Context.BlockNumber)
return nil, nil
}
func opDifficulty(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- v, _ := uint256.FromBig(evm.Context.Difficulty)
- scope.Stack.push(v)
+ scope.Stack.get().SetFromBig(evm.Context.Difficulty)
return nil, nil
}
func opRandom(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- v := new(uint256.Int).SetBytes(evm.Context.Random.Bytes())
- scope.Stack.push(v)
+ scope.Stack.get().SetBytes(evm.Context.Random.Bytes())
return nil, nil
}
func opGasLimit(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(evm.Context.GasLimit))
+ scope.Stack.get().SetUint64(evm.Context.GasLimit)
return nil, nil
}
@@ -556,17 +552,17 @@ func opJumpdest(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opPc(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(*pc))
+ scope.Stack.get().SetUint64(*pc)
return nil, nil
}
func opMsize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(uint64(scope.Memory.Len())))
+ scope.Stack.get().SetUint64(uint64(scope.Memory.Len()))
return nil, nil
}
func opGas(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas.RegularGas))
+ scope.Stack.get().SetUint64(scope.Contract.Gas.RegularGas)
return nil, nil
}
@@ -667,9 +663,9 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// reuse size int for stackvalue
stackvalue := size
- scope.Contract.UseGas(gas, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
+ scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
- res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, gas, &value)
+ res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value)
// Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must
@@ -707,10 +703,10 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Apply EIP150
gas -= gas / 64
- scope.Contract.UseGas(gas, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
+ scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
// reuse size int for stackvalue
stackvalue := size
- res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, gas,
+ res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas),
&endowment, &salt)
// Push item on the stack based on the returned error.
if suberr != nil {
@@ -747,7 +743,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() {
gas += params.CallStipend
}
- ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, gas, &value)
+ ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
if err != nil {
temp.Clear()
@@ -781,7 +777,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
gas += params.CallStipend
}
- ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, gas, &value)
+ ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
if err != nil {
temp.Clear()
} else {
@@ -810,7 +806,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, gas, scope.Contract.value)
+ ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value)
if err != nil {
temp.Clear()
} else {
@@ -839,7 +835,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, gas)
+ ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas))
if err != nil {
temp.Clear()
} else {
@@ -996,7 +992,8 @@ func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// This range is excluded to preserve compatibility with existing opcodes.
if x > 90 && x < 128 {
- return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
+ operand := x
+ return nil, &ErrInvalidOpCode{opcode: DUPN, operand: &operand}
}
n := decodeSingle(x)
@@ -1006,7 +1003,7 @@ func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
//The n‘th stack item is duplicated at the top of the stack.
- scope.Stack.push(scope.Stack.Back(n - 1))
+ scope.Stack.push(scope.Stack.back(n - 1))
*pc += 1
return nil, nil
}
@@ -1023,7 +1020,8 @@ func opSwapN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// This range is excluded to preserve compatibility with existing opcodes.
if x > 90 && x < 128 {
- return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
+ operand := x
+ return nil, &ErrInvalidOpCode{opcode: SWAPN, operand: &operand}
}
n := decodeSingle(x)
@@ -1032,10 +1030,10 @@ func opSwapN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: n + 1}
}
- // The (n+1)‘th stack item is swapped with the top of the stack.
- indexTop := scope.Stack.len() - 1
- indexN := scope.Stack.len() - 1 - n
- scope.Stack.data[indexTop], scope.Stack.data[indexN] = scope.Stack.data[indexN], scope.Stack.data[indexTop]
+ // The (n+1)’th stack item is swapped with the top of the stack.
+ top := scope.Stack.peek()
+ nth := scope.Stack.back(n)
+ *top, *nth = *nth, *top
*pc += 1
return nil, nil
}
@@ -1053,7 +1051,8 @@ func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// This range is excluded both to preserve compatibility with existing opcodes
// and to keep decode_pair’s 16-aligned arithmetic mapping valid (0–81, 128–255).
if x > 81 && x < 128 {
- return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
+ operand := x
+ return nil, &ErrInvalidOpCode{opcode: EXCHANGE, operand: &operand}
}
n, m := decodePair(x)
need := max(n, m) + 1
@@ -1064,10 +1063,10 @@ func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: need}
}
- // The (n+1)‘th stack item is swapped with the (m+1)‘th stack item.
- indexN := scope.Stack.len() - 1 - n
- indexM := scope.Stack.len() - 1 - m
- scope.Stack.data[indexN], scope.Stack.data[indexM] = scope.Stack.data[indexM], scope.Stack.data[indexN]
+ // The (n+1)’th stack item is swapped with the (m+1)’th stack item.
+ nth := scope.Stack.back(n)
+ mth := scope.Stack.back(m)
+ *nth, *mth = *mth, *nth
*pc += 1
return nil, nil
}
@@ -1103,13 +1102,13 @@ func makeLog(size int) executionFunc {
func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
codeLen = uint64(len(scope.Contract.Code))
- integer = new(uint256.Int)
+ elem = scope.Stack.get()
)
*pc += 1
if *pc < codeLen {
- scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc])))
+ elem.SetUint64(uint64(scope.Contract.Code[*pc]))
} else {
- scope.Stack.push(integer.Clear())
+ elem.Clear()
}
return nil, nil
}
@@ -1118,14 +1117,14 @@ func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
func opPush2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
codeLen = uint64(len(scope.Contract.Code))
- integer = new(uint256.Int)
+ elem = scope.Stack.get()
)
if *pc+2 < codeLen {
- scope.Stack.push(integer.SetBytes2(scope.Contract.Code[*pc+1 : *pc+3]))
+ elem.SetBytes2(scope.Contract.Code[*pc+1 : *pc+3])
} else if *pc+1 < codeLen {
- scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc+1]) << 8))
+ elem.SetUint64(uint64(scope.Contract.Code[*pc+1]) << 8)
} else {
- scope.Stack.push(integer.Clear())
+ elem.Clear()
}
*pc += 2
return nil, nil
@@ -1139,13 +1138,13 @@ func makePush(size uint64, pushByteSize int) executionFunc {
start = min(codeLen, int(*pc+1))
end = min(codeLen, start+pushByteSize)
)
- a := new(uint256.Int).SetBytes(scope.Contract.Code[start:end])
+ a := scope.Stack.get()
+ a.SetBytes(scope.Contract.Code[start:end])
// Missing bytes: pushByteSize - len(pushData)
if missing := pushByteSize - (end - start); missing > 0 {
a.Lsh(a, uint(8*missing))
}
- scope.Stack.push(a)
*pc += size
return nil, nil
}
diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go
index 1f69eea3da..354d2ce5ab 100644
--- a/core/vm/instructions_test.go
+++ b/core/vm/instructions_test.go
@@ -25,6 +25,7 @@ import (
"os"
"strings"
"testing"
+ "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
@@ -98,7 +99,7 @@ func init() {
func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
@@ -109,8 +110,8 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
stack.push(x)
stack.push(y)
opFn(&pc, evm, &ScopeContext{nil, stack, nil})
- if len(stack.data) != 1 {
- t.Errorf("Expected one item on stack after %v, got %d: ", name, len(stack.data))
+ if stack.len() != 1 {
+ t.Errorf("Expected one item on stack after %v, got %d: ", name, stack.len())
}
actual := stack.pop()
@@ -196,7 +197,7 @@ func TestSAR(t *testing.T) {
func TestAddMod(t *testing.T) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
tests := []struct {
@@ -239,7 +240,7 @@ func TestWriteExpectedValues(t *testing.T) {
getResult := func(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
result := make([]TwoOperandTestcase, len(args))
@@ -282,23 +283,40 @@ func TestJsonTestcases(t *testing.T) {
func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
var (
- evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
- scope = &ScopeContext{nil, stack, nil}
+ evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
+ stack = newStackForTesting()
+ code = []byte{}
+ opPush32 = makePush(32, 32)
)
// convert args
intArgs := make([]*uint256.Int, len(args))
for i, arg := range args {
+ code = append(code, common.LeftPadBytes(common.Hex2Bytes(arg), 32)...)
intArgs[i] = new(uint256.Int).SetBytes(common.Hex2Bytes(arg))
}
pc := uint64(0)
- for bench.Loop() {
- for _, arg := range intArgs {
- stack.push(arg)
+ scope := &ScopeContext{nil, stack, &Contract{Code: code}}
+ start := time.Now()
+ bench.ResetTimer()
+ for i := 0; i < bench.N; i++ {
+ for range len(args) {
+ opPush32(&pc, evm, scope)
+ pc += 32
}
op(&pc, evm, scope)
- stack.pop()
+ opPop(&pc, evm, scope)
}
+ bench.StopTimer()
+ elapsed := uint64(time.Since(start))
+ if elapsed < 1 {
+ elapsed = 1
+ }
+ reqGas := uint64(len(args))*GasFastestStep + GasFastestStep + GasQuickStep
+ gasUsed := reqGas * uint64(bench.N)
+ bench.ReportMetric(float64(reqGas), "gas/op")
+ // Keep it as uint64, multiply 100 to get two digit float later
+ mgasps := (100 * 1000 * gasUsed) / elapsed
+ bench.ReportMetric(float64(mgasps)/100, "mgas/s")
for i, arg := range args {
want := new(uint256.Int).SetBytes(common.Hex2Bytes(arg))
@@ -519,7 +537,7 @@ func BenchmarkOpIsZero(b *testing.B) {
func TestOpMstore(t *testing.T) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
mem = NewMemory()
)
mem.Resize(64)
@@ -542,7 +560,7 @@ func TestOpMstore(t *testing.T) {
func BenchmarkOpMstore(bench *testing.B) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
mem = NewMemory()
)
mem.Resize(64)
@@ -561,11 +579,11 @@ func TestOpTstore(t *testing.T) {
var (
statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
evm = NewEVM(BlockContext{}, statedb, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
mem = NewMemory()
caller = common.Address{}
to = common.Address{1}
- contract = NewContract(caller, to, new(uint256.Int), 0, nil)
+ contract = NewContract(caller, to, new(uint256.Int), GasBudget{}, nil)
scopeContext = ScopeContext{mem, stack, contract}
value = common.Hex2Bytes("abcdef00000000000000abba000000000deaf000000c0de00100000000133700")
)
@@ -600,7 +618,7 @@ func TestOpTstore(t *testing.T) {
func BenchmarkOpKeccak256(bench *testing.B) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
mem = NewMemory()
)
mem.Resize(32)
@@ -672,7 +690,7 @@ func TestCreate2Addresses(t *testing.T) {
codeHash := crypto.Keccak256(code)
address := crypto.CreateAddress2(origin, salt, codeHash)
/*
- stack := newstack()
+ stack := newStackForTesting()
// salt, but we don't need that for this test
stack.push(big.NewInt(int64(len(code)))) //size
stack.push(big.NewInt(0)) // memstart
@@ -701,12 +719,12 @@ func TestRandom(t *testing.T) {
} {
var (
evm = NewEVM(BlockContext{Random: &tt.random}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
opRandom(&pc, evm, &ScopeContext{nil, stack, nil})
- if len(stack.data) != 1 {
- t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data))
+ if have, want := stack.len(), 1; have != want {
+ t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want)
}
actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes()))
@@ -741,14 +759,14 @@ func TestBlobHash(t *testing.T) {
} {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
evm.SetTxContext(TxContext{BlobHashes: tt.hashes})
stack.push(uint256.NewInt(tt.idx))
opBlobHash(&pc, evm, &ScopeContext{nil, stack, nil})
- if len(stack.data) != 1 {
- t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data))
+ if have, want := stack.len(), 1; have != want {
+ t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want)
}
actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.expect.Bytes()))
@@ -844,7 +862,7 @@ func TestOpMCopy(t *testing.T) {
} {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
- stack = newstack()
+ stack = newStackForTesting()
pc = uint64(0)
)
data := common.FromHex(strings.ReplaceAll(tc.pre, " ", ""))
@@ -907,7 +925,7 @@ func TestPush(t *testing.T) {
scope := &ScopeContext{
Memory: nil,
- Stack: newstack(),
+ Stack: newStackForTesting(),
Contract: &Contract{
Code: code,
},
@@ -988,7 +1006,7 @@ func TestOpCLZ(t *testing.T) {
}
for _, tc := range tests {
// prepare a fresh stack and PC
- stack := newstack()
+ stack := newStackForTesting()
pc := uint64(0)
// parse input
@@ -1014,11 +1032,12 @@ func TestEIP8024_Execution(t *testing.T) {
evm := NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
tests := []struct {
- name string
- codeHex string
- wantErr error
- wantOpcode OpCode
- wantVals []uint64
+ name string
+ codeHex string
+ wantErr error
+ wantOpcode OpCode
+ wantOperand *byte
+ wantVals []uint64
}{
{
name: "DUPN",
@@ -1063,10 +1082,18 @@ func TestEIP8024_Execution(t *testing.T) {
},
},
{
- name: "INVALID_SWAPN_LOW",
- codeHex: "e75b",
- wantErr: &ErrInvalidOpCode{},
- wantOpcode: SWAPN,
+ name: "INVALID_DUPN_LOW",
+ codeHex: "e65b",
+ wantErr: &ErrInvalidOpCode{},
+ wantOpcode: DUPN,
+ wantOperand: ptrToByte(0x5b),
+ },
+ {
+ name: "INVALID_SWAPN_LOW",
+ codeHex: "e75b",
+ wantErr: &ErrInvalidOpCode{},
+ wantOpcode: SWAPN,
+ wantOperand: ptrToByte(0x5b),
},
{
name: "JUMP_OVER_INVALID_DUPN",
@@ -1079,10 +1106,11 @@ func TestEIP8024_Execution(t *testing.T) {
wantVals: []uint64{1, 0, 0},
},
{
- name: "INVALID_EXCHANGE",
- codeHex: "e852",
- wantErr: &ErrInvalidOpCode{},
- wantOpcode: EXCHANGE,
+ name: "INVALID_EXCHANGE",
+ codeHex: "e852",
+ wantErr: &ErrInvalidOpCode{},
+ wantOpcode: EXCHANGE,
+ wantOperand: ptrToByte(0x52),
},
{
name: "UNDERFLOW_DUPN",
@@ -1101,7 +1129,7 @@ func TestEIP8024_Execution(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
code := common.FromHex(tc.codeHex)
- stack := newstack()
+ stack := newStackForTesting()
pc := uint64(0)
scope := &ScopeContext{Stack: stack, Contract: &Contract{Code: code}}
var err error
@@ -1150,10 +1178,21 @@ func TestEIP8024_Execution(t *testing.T) {
// Fail if we don't get the error we expect.
switch tc.wantErr.(type) {
case *ErrInvalidOpCode:
- var want *ErrInvalidOpCode
- if !errors.As(err, &want) {
+ var got *ErrInvalidOpCode
+ if !errors.As(err, &got) {
t.Fatalf("expected ErrInvalidOpCode, got %v", err)
}
+ if got.opcode != tc.wantOpcode {
+ t.Fatalf("ErrInvalidOpCode.opcode=%s; want %s", got.opcode, tc.wantOpcode)
+ }
+ if tc.wantOperand != nil {
+ if got.operand == nil {
+ t.Fatalf("ErrInvalidOpCode.operand=nil; want 0x%02x", *tc.wantOperand)
+ }
+ if *got.operand != *tc.wantOperand {
+ t.Fatalf("ErrInvalidOpCode.operand=0x%02x; want 0x%02x", *got.operand, *tc.wantOperand)
+ }
+ }
case *ErrStackUnderflow:
var want *ErrStackUnderflow
if !errors.As(err, &want) {
@@ -1168,8 +1207,9 @@ func TestEIP8024_Execution(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
got := make([]uint64, 0, stack.len())
- for i := stack.len() - 1; i >= 0; i-- {
- got = append(got, stack.data[i].Uint64())
+ data := stack.Data()
+ for i := len(data) - 1; i >= 0; i-- {
+ got = append(got, data[i].Uint64())
}
if len(got) != len(tc.wantVals) {
t.Fatalf("stack len=%d; want %d", len(got), len(tc.wantVals))
@@ -1183,3 +1223,8 @@ func TestEIP8024_Execution(t *testing.T) {
})
}
}
+
+func ptrToByte(v byte) *byte {
+ b := v
+ return &b
+}
diff --git a/core/vm/interface.go b/core/vm/interface.go
index d7c4340e06..a9938c2a28 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/core/stateless"
"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/params"
"github.com/holiman/uint256"
)
@@ -52,7 +53,6 @@ type StateDB interface {
GetStateAndCommittedState(common.Address, common.Hash) (common.Hash, common.Hash)
GetState(common.Address, common.Hash) common.Hash
SetState(common.Address, common.Hash, common.Hash) common.Hash
- GetStorageRoot(addr common.Address) common.Hash
GetTransientState(addr common.Address, key common.Hash) common.Hash
SetTransientState(addr common.Address, key, value common.Hash)
@@ -64,6 +64,9 @@ type StateDB interface {
// Notably this also returns true for self-destructed accounts within the current transaction.
Exist(common.Address) bool
+ // Touch accesses the state without returning anything.
+ Touch(common.Address)
+
// IsNewContract reports whether the contract at the given address was deployed
// during the current transaction.
IsNewContract(addr common.Address) bool
@@ -95,5 +98,6 @@ type StateDB interface {
AccessEvents() *state.AccessEvents
// Finalise must be invoked at the end of a transaction
- Finalise(bool)
+ 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 b507595fab..3994327247 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -116,8 +116,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
var (
op OpCode // current opcode
jumpTable *JumpTable = evm.table
- mem = NewMemory() // bound memory
- stack = newstack() // local stack
+ mem = NewMemory() // bound memory
+ stack = evm.arena.stack() // local stack
callContext = &ScopeContext{
Memory: mem,
Stack: stack,
@@ -140,7 +140,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// so that it gets executed _after_: the OnOpcode needs the stacks before
// they are returned to the pools
defer func() {
- returnStack(stack)
+ stack.release()
mem.Free()
}()
contract.Input = input
@@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// associated costs.
contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
- contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -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/core/vm/interpreter_test.go b/core/vm/interpreter_test.go
index 28df8546b5..868cb12d04 100644
--- a/core/vm/interpreter_test.go
+++ b/core/vm/interpreter_test.go
@@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) {
timeout := make(chan bool)
go func(evm *EVM) {
- _, _, err := evm.Call(common.Address{}, address, nil, math.MaxUint64, new(uint256.Int))
+ _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int))
errChannel <- err
}(evm)
@@ -83,9 +83,9 @@ func BenchmarkInterpreter(b *testing.B) {
evm = NewEVM(BlockContext{BlockNumber: big.NewInt(1), Time: 1, Random: &common.Hash{}}, statedb, params.MergedTestChainConfig, Config{})
startGas uint64 = 100_000_000
value = uint256.NewInt(0)
- stack = newstack()
+ stack = newStackForTesting()
mem = NewMemory()
- contract = NewContract(common.Address{}, common.Address{}, value, startGas, nil)
+ contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil)
)
stack.push(uint256.NewInt(123))
stack.push(uint256.NewInt(123))
diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go
index fdf814d64c..a4a99ea498 100644
--- a/core/vm/jump_table_export.go
+++ b/core/vm/jump_table_export.go
@@ -26,7 +26,7 @@ import (
// the rules.
func LookupInstructionSet(rules params.Rules) (JumpTable, error) {
switch {
- case rules.IsVerkle:
+ case rules.IsUBT:
return newCancunInstructionSet(), errors.New("verkle-fork not defined yet")
case rules.IsAmsterdam:
return newAmsterdamInstructionSet(), nil
diff --git a/core/vm/memory_table.go b/core/vm/memory_table.go
index 63ad967850..8f30cbeee6 100644
--- a/core/vm/memory_table.go
+++ b/core/vm/memory_table.go
@@ -17,59 +17,59 @@
package vm
func memoryKeccak256(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(1))
+ return calcMemSize64(stack.back(0), stack.back(1))
}
func memoryCallDataCopy(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(2))
+ return calcMemSize64(stack.back(0), stack.back(2))
}
func memoryReturnDataCopy(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(2))
+ return calcMemSize64(stack.back(0), stack.back(2))
}
func memoryCodeCopy(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(2))
+ return calcMemSize64(stack.back(0), stack.back(2))
}
func memoryExtCodeCopy(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(1), stack.Back(3))
+ return calcMemSize64(stack.back(1), stack.back(3))
}
func memoryMLoad(stack *Stack) (uint64, bool) {
- return calcMemSize64WithUint(stack.Back(0), 32)
+ return calcMemSize64WithUint(stack.back(0), 32)
}
func memoryMStore8(stack *Stack) (uint64, bool) {
- return calcMemSize64WithUint(stack.Back(0), 1)
+ return calcMemSize64WithUint(stack.back(0), 1)
}
func memoryMStore(stack *Stack) (uint64, bool) {
- return calcMemSize64WithUint(stack.Back(0), 32)
+ return calcMemSize64WithUint(stack.back(0), 32)
}
func memoryMcopy(stack *Stack) (uint64, bool) {
- mStart := stack.Back(0) // stack[0]: dest
- if stack.Back(1).Gt(mStart) {
- mStart = stack.Back(1) // stack[1]: source
+ mStart := stack.back(0) // stack[0]: dest
+ if stack.back(1).Gt(mStart) {
+ mStart = stack.back(1) // stack[1]: source
}
- return calcMemSize64(mStart, stack.Back(2)) // stack[2]: length
+ return calcMemSize64(mStart, stack.back(2)) // stack[2]: length
}
func memoryCreate(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(1), stack.Back(2))
+ return calcMemSize64(stack.back(1), stack.back(2))
}
func memoryCreate2(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(1), stack.Back(2))
+ return calcMemSize64(stack.back(1), stack.back(2))
}
func memoryCall(stack *Stack) (uint64, bool) {
- x, overflow := calcMemSize64(stack.Back(5), stack.Back(6))
+ x, overflow := calcMemSize64(stack.back(5), stack.back(6))
if overflow {
return 0, true
}
- y, overflow := calcMemSize64(stack.Back(3), stack.Back(4))
+ y, overflow := calcMemSize64(stack.back(3), stack.back(4))
if overflow {
return 0, true
}
@@ -80,11 +80,11 @@ func memoryCall(stack *Stack) (uint64, bool) {
}
func memoryDelegateCall(stack *Stack) (uint64, bool) {
- x, overflow := calcMemSize64(stack.Back(4), stack.Back(5))
+ x, overflow := calcMemSize64(stack.back(4), stack.back(5))
if overflow {
return 0, true
}
- y, overflow := calcMemSize64(stack.Back(2), stack.Back(3))
+ y, overflow := calcMemSize64(stack.back(2), stack.back(3))
if overflow {
return 0, true
}
@@ -95,11 +95,11 @@ func memoryDelegateCall(stack *Stack) (uint64, bool) {
}
func memoryStaticCall(stack *Stack) (uint64, bool) {
- x, overflow := calcMemSize64(stack.Back(4), stack.Back(5))
+ x, overflow := calcMemSize64(stack.back(4), stack.back(5))
if overflow {
return 0, true
}
- y, overflow := calcMemSize64(stack.Back(2), stack.Back(3))
+ y, overflow := calcMemSize64(stack.back(2), stack.back(3))
if overflow {
return 0, true
}
@@ -110,13 +110,13 @@ func memoryStaticCall(stack *Stack) (uint64, bool) {
}
func memoryReturn(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(1))
+ return calcMemSize64(stack.back(0), stack.back(1))
}
func memoryRevert(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(1))
+ return calcMemSize64(stack.back(0), stack.back(1))
}
func memoryLog(stack *Stack) (uint64, bool) {
- return calcMemSize64(stack.Back(0), stack.Back(1))
+ return calcMemSize64(stack.back(0), stack.back(1))
}
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index 154c261cae..86ac262a93 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -37,7 +37,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
}
// Gas sentry honoured, do the actual gas calculation based on the stored value
var (
- y, x = stack.Back(1), stack.peek()
+ y, x = stack.back(1), stack.peek()
slot = common.Hash(x.Bytes32())
current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot)
cost = uint64(0)
@@ -158,7 +158,7 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
- addr := common.Address(stack.Back(addressPosition).Bytes20())
+ addr := common.Address(stack.back(addressPosition).Bytes20())
// Check slot presence in the access list
warmAccess := evm.StateDB.AddressInAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
@@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
// gas for call
- if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -269,7 +269,7 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
// Although it's checked in `gasCall`, EIP-7702 loads the target's code before
// to determine if it is resolving a delegation. This could incorrectly record
// the target in the block access list (BAL) if the call later fails.
- transfersValue := !stack.Back(2).IsZero()
+ transfersValue := !stack.back(2).IsZero()
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
}
@@ -281,7 +281,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
var (
eip2929Cost uint64
eip7702Cost uint64
- addr = common.Address(stack.Back(1).Bytes20())
+ addr = common.Address(stack.back(1).Bytes20())
)
// Perform EIP-2929 checks (stateless), checking address presence
// in the accessList and charge the cold access accordingly.
@@ -295,7 +295,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Charge the remaining difference here already, to correctly calculate
// available gas for call
- if !contract.UseGas(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.UseGas(GasCosts{RegularGas: eip2929Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -324,13 +324,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = params.ColdAccountAccessCostEIP2929
}
- if !contract.UseGas(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.UseGas(GasCosts{RegularGas: eip7702Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied.
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.Back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0))
if err != nil {
return GasCosts{}, err
}
diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go
index d57f2c4dcf..4d3960a174 100644
--- a/core/vm/operations_verkle.go
+++ b/core/vm/operations_verkle.go
@@ -56,7 +56,7 @@ func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
- target = common.Address(stack.Back(1).Bytes20())
+ target = common.Address(stack.back(1).Bytes20())
witnessGas uint64
_, isPrecompile = evm.precompile(target)
isSystemContract = target == params.HistoryStorageAddress
@@ -64,7 +64,7 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga
// If value is transferred, it is charged before 1/64th
// is subtracted from the available gas pool.
- if withTransferCosts && !stack.Back(2).IsZero() {
+ if withTransferCosts && !stack.back(2).IsZero() {
wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas.RegularGas)
if wantedValueTransferWitnessGas > contract.Gas.RegularGas {
return GasCosts{RegularGas: wantedValueTransferWitnessGas}, nil
@@ -168,8 +168,8 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
gas := gasCost.RegularGas
if !contract.IsDeployment && !contract.IsSystemCall {
var (
- codeOffset = stack.Back(1)
- length = stack.Back(2)
+ codeOffset = stack.back(1)
+ length = stack.back(2)
)
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow {
diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go
index b40e99d047..4fafdf3a50 100644
--- a/core/vm/runtime/runtime.go
+++ b/core/vm/runtime/runtime.go
@@ -109,7 +109,9 @@ func setDefaults(cfg *Config) {
if cfg.BlobBaseFee == nil {
cfg.BlobBaseFee = big.NewInt(params.BlobTxMinBlobGasprice)
}
- cfg.Random = &(common.Hash{})
+ if cfg.Random == nil {
+ cfg.Random = new(common.Hash)
+ }
}
// Execute executes the code using the input as call data during the execution.
@@ -146,11 +148,11 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
cfg.Origin,
common.BytesToAddress([]byte("contract")),
input,
- cfg.GasLimit,
+ vm.NewGasBudget(cfg.GasLimit),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
}
return ret, cfg.State, err
}
@@ -180,13 +182,13 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
code, address, leftOverGas, err := vmenv.Create(
cfg.Origin,
input,
- cfg.GasLimit,
+ vm.NewGasBudget(cfg.GasLimit),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
}
- return code, address, leftOverGas, err
+ return code, address, leftOverGas.RegularGas, err
}
// Call executes the code given by the contract's address. It will return the
@@ -215,11 +217,11 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er
cfg.Origin,
address,
input,
- cfg.GasLimit,
+ vm.NewGasBudget(cfg.GasLimit),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
}
- return ret, leftOverGas, err
+ return ret, leftOverGas.RegularGas, err
}
diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go
index a001d81623..bc2ffd622d 100644
--- a/core/vm/runtime/runtime_test.go
+++ b/core/vm/runtime/runtime_test.go
@@ -65,6 +65,21 @@ func TestDefaults(t *testing.T) {
if cfg.BlockNumber == nil {
t.Error("expected block number to be non nil")
}
+ if cfg.Random == nil {
+ t.Error("expected Random to be non nil")
+ }
+}
+
+func TestDefaultsPreserveRandom(t *testing.T) {
+ h := common.HexToHash("0x01")
+ cfg := &Config{Random: &h}
+ setDefaults(cfg)
+ if cfg.Random == nil {
+ t.Fatal("expected Random to remain non-nil")
+ }
+ if *cfg.Random != h {
+ t.Fatalf("expected Random to be preserved, got %x, want %x", *cfg.Random, h)
+ }
}
func TestEVM(t *testing.T) {
@@ -944,3 +959,63 @@ func TestDelegatedAccountAccessCost(t *testing.T) {
}
}
}
+
+func TestManyLargeStacks(t *testing.T) {
+ // This piece of code will push 512 items to the stack, and then call itself
+ // recursively.
+ code := make([]byte, 10)
+ for i := range code {
+ code[i] = byte(vm.PUSH0)
+ }
+ code = append(code, []byte{
+ byte(vm.ADDRESS), // address to call
+ byte(vm.GAS),
+ byte(vm.CALL),
+ }...)
+
+ main := common.HexToAddress("0xbb")
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.SetCode(main, code, tracing.CodeChangeUnspecified)
+
+ //tracer := logger.NewJSONLogger(nil, os.Stdout)
+ var tracer *tracing.Hooks
+ _, _, err := Call(main, nil, &Config{
+ GasLimit: 10_000_000,
+ State: statedb,
+ EVMConfig: vm.Config{
+ Tracer: tracer,
+ }})
+ if err != nil {
+ t.Fatal("didn't expect error", err)
+ }
+}
+
+func BenchmarkLargeDeepStacks(b *testing.B) {
+ // This piece of code will push 512 items to the stack, and then call itself
+ // recursively.
+ code := make([]byte, 512)
+ for i := range code {
+ code[i] = byte(vm.PUSH0)
+ }
+ code = append(code, []byte{
+ byte(vm.ADDRESS), // address to call
+ byte(vm.GAS),
+ byte(vm.CALL),
+ }...)
+ benchmarkNonModifyingCode(10_000_000, code, "deep-large-stacks-10M", "", b)
+}
+
+func BenchmarkShortDeepStacks(b *testing.B) {
+ // This piece of code will push a few items to the stack, and then call itself
+ // recursively.
+ code := make([]byte, 8)
+ for i := range code {
+ code[i] = byte(vm.PUSH0)
+ }
+ code = append(code, []byte{
+ byte(vm.ADDRESS), // address to call
+ byte(vm.GAS),
+ byte(vm.CALL),
+ }...)
+ benchmarkNonModifyingCode(10_000_000, code, "deep-short-stacks-10M", "", b)
+}
diff --git a/core/vm/stack.go b/core/vm/stack.go
index 879dc9aa6d..d8000bc86d 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -17,111 +17,170 @@
package vm
import (
+ "slices"
"sync"
"github.com/holiman/uint256"
)
+// stackArena is an arena which actual evm stacks use for data storage
+type stackArena struct {
+ data []uint256.Int
+ top int // first free slot
+}
+
+func newArena() *stackArena {
+ return stackPool.Get().(*stackArena)
+}
+
+// 1025, because in stack() there is a condition check
+// for the stack size that would fail if it was set to
+// 1024.
+const initialStackSize = 1025
+
var stackPool = sync.Pool{
- New: func() interface{} {
- return &Stack{data: make([]uint256.Int, 0, 16)}
+ New: func() any {
+ return &stackArena{
+ data: make([]uint256.Int, initialStackSize),
+ }
},
}
+func returnStack(arena *stackArena) {
+ arena.top = 0 // defensive, not strictly needed as s.inner.top = s.bottom in release()
+ stackPool.Put(arena)
+}
+
+// stack returns an instance of a stack which uses the underlying arena. The instance
+// must be released by invoking the (*Stack).release() method
+func (sa *stackArena) stack() *Stack {
+ // make sure every substack has at least 1024 elements
+ if len(sa.data) <= sa.top+1024 {
+ // we need to grow the arena
+ sa.data = slices.Grow(sa.data, 1024)
+ sa.data = sa.data[:cap(sa.data)]
+ }
+ return &Stack{
+ bottom: sa.top,
+ size: 0,
+ inner: sa,
+ }
+}
+
+// newStackForTesting is meant to be used solely for testing. It creates a stack
+// backed by a newly allocated arena.
+func newStackForTesting() *Stack {
+ arena := &stackArena{
+ data: make([]uint256.Int, 1025),
+ }
+ return arena.stack()
+}
+
// Stack is an object for basic stack operations. Items popped to the stack are
// expected to be changed and modified. stack does not take care of adding newly
// initialized objects.
type Stack struct {
- data []uint256.Int
+ bottom int // bottom is the index of the first element of this stack
+ size int // size is the number of elements in this stack
+ inner *stackArena
}
-func newstack() *Stack {
- return stackPool.Get().(*Stack)
-}
-
-func returnStack(s *Stack) {
- s.data = s.data[:0]
- stackPool.Put(s)
+// release un-claims the area of the arena which was claimed by the stack.
+func (s *Stack) release() {
+ // When the stack is returned, need to notify the arena that the new 'top' is
+ // the returned stack's bottom.
+ s.inner.top = s.bottom
}
// Data returns the underlying uint256.Int array.
-func (st *Stack) Data() []uint256.Int {
- return st.data
+func (s *Stack) Data() []uint256.Int {
+ return s.inner.data[s.bottom : s.bottom+s.size]
}
-func (st *Stack) push(d *uint256.Int) {
- // NOTE push limit (1024) is checked in baseCheck
- st.data = append(st.data, *d)
+func (s *Stack) push(d *uint256.Int) {
+ elem := s.get()
+ *elem = *d
}
-func (st *Stack) pop() (ret uint256.Int) {
- ret = st.data[len(st.data)-1]
- st.data = st.data[:len(st.data)-1]
- return
+// get returns a pointer to a newly created element
+// on top of the stack
+func (s *Stack) get() *uint256.Int {
+ elem := &s.inner.data[s.inner.top]
+ s.inner.top++
+ s.size++
+ return elem
}
-func (st *Stack) len() int {
- return len(st.data)
+func (s *Stack) pop() uint256.Int {
+ s.inner.top--
+ s.size--
+ return s.inner.data[s.inner.top]
}
-func (st *Stack) swap1() {
- st.data[st.len()-2], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-2]
-}
-func (st *Stack) swap2() {
- st.data[st.len()-3], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-3]
-}
-func (st *Stack) swap3() {
- st.data[st.len()-4], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-4]
-}
-func (st *Stack) swap4() {
- st.data[st.len()-5], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-5]
-}
-func (st *Stack) swap5() {
- st.data[st.len()-6], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-6]
-}
-func (st *Stack) swap6() {
- st.data[st.len()-7], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-7]
-}
-func (st *Stack) swap7() {
- st.data[st.len()-8], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-8]
-}
-func (st *Stack) swap8() {
- st.data[st.len()-9], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-9]
-}
-func (st *Stack) swap9() {
- st.data[st.len()-10], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-10]
-}
-func (st *Stack) swap10() {
- st.data[st.len()-11], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-11]
-}
-func (st *Stack) swap11() {
- st.data[st.len()-12], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-12]
-}
-func (st *Stack) swap12() {
- st.data[st.len()-13], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-13]
-}
-func (st *Stack) swap13() {
- st.data[st.len()-14], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-14]
-}
-func (st *Stack) swap14() {
- st.data[st.len()-15], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-15]
-}
-func (st *Stack) swap15() {
- st.data[st.len()-16], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-16]
-}
-func (st *Stack) swap16() {
- st.data[st.len()-17], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-17]
+func (s *Stack) len() int {
+ return s.size
}
-func (st *Stack) dup(n int) {
- st.push(&st.data[st.len()-n])
+func (s *Stack) swap1() {
+ s.inner.data[s.bottom+s.size-2], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-2]
+}
+func (s *Stack) swap2() {
+ s.inner.data[s.bottom+s.size-3], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-3]
+}
+func (s *Stack) swap3() {
+ s.inner.data[s.bottom+s.size-4], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-4]
+}
+func (s *Stack) swap4() {
+ s.inner.data[s.bottom+s.size-5], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-5]
+}
+func (s *Stack) swap5() {
+ s.inner.data[s.bottom+s.size-6], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-6]
+}
+func (s *Stack) swap6() {
+ s.inner.data[s.bottom+s.size-7], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-7]
+}
+func (s *Stack) swap7() {
+ s.inner.data[s.bottom+s.size-8], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-8]
+}
+func (s *Stack) swap8() {
+ s.inner.data[s.bottom+s.size-9], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-9]
+}
+func (s *Stack) swap9() {
+ s.inner.data[s.bottom+s.size-10], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-10]
+}
+func (s *Stack) swap10() {
+ s.inner.data[s.bottom+s.size-11], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-11]
+}
+func (s *Stack) swap11() {
+ s.inner.data[s.bottom+s.size-12], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-12]
+}
+func (s *Stack) swap12() {
+ s.inner.data[s.bottom+s.size-13], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-13]
+}
+func (s *Stack) swap13() {
+ s.inner.data[s.bottom+s.size-14], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-14]
+}
+func (s *Stack) swap14() {
+ s.inner.data[s.bottom+s.size-15], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-15]
+}
+func (s *Stack) swap15() {
+ s.inner.data[s.bottom+s.size-16], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-16]
+}
+func (s *Stack) swap16() {
+ s.inner.data[s.bottom+s.size-17], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-17]
}
-func (st *Stack) peek() *uint256.Int {
- return &st.data[st.len()-1]
+func (s *Stack) dup(n int) {
+ s.inner.data[s.bottom+s.size] = s.inner.data[s.bottom+s.size-n]
+ s.size++
+ s.inner.top++
}
-// Back returns the n'th item in stack
-func (st *Stack) Back(n int) *uint256.Int {
- return &st.data[st.len()-n-1]
+func (s *Stack) peek() *uint256.Int {
+ return &s.inner.data[s.bottom+s.size-1]
+}
+
+// back returns the n'th item in stack
+func (s *Stack) back(n int) *uint256.Int {
+ return &s.inner.data[s.bottom+s.size-n-1]
}
diff --git a/crypto/bn256/google/gfp2.go b/crypto/bn256/google/gfp2.go
index 3981f6cb4f..394ef83548 100644
--- a/crypto/bn256/google/gfp2.go
+++ b/crypto/bn256/google/gfp2.go
@@ -153,7 +153,7 @@ func (e *gfP2) MulScalar(a *gfP2, b *big.Int) *gfP2 {
// MulXi sets e=ξa where ξ=i+9 and then returns e.
func (e *gfP2) MulXi(a *gfP2, pool *bnPool) *gfP2 {
- // (xi+y)(i+3) = (9x+y)i+(9y-x)
+ // (xi+y)(i+9) = (9x+y)i+(9y-x)
tx := pool.Get().Lsh(a.x, 3)
tx.Add(tx, a.x)
tx.Add(tx, a.y)
diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go
index d803ab27c5..0636427a4f 100644
--- a/crypto/crypto_test.go
+++ b/crypto/crypto_test.go
@@ -293,7 +293,7 @@ func TestPythonIntegration(t *testing.T) {
sig0, _ := Sign(msg0, k0)
msg1 := common.FromHex("00000000000000000000000000000000")
- sig1, _ := Sign(msg0, k0)
+ sig1, _ := Sign(msg1, k0)
t.Logf("msg: %x, privkey: %s sig: %x\n", msg0, kh, sig0)
t.Logf("msg: %x, privkey: %s sig: %x\n", msg1, kh, sig1)
diff --git a/crypto/kzg4844/kzg4844.go b/crypto/kzg4844/kzg4844.go
index 3ccc204838..1e021d64a6 100644
--- a/crypto/kzg4844/kzg4844.go
+++ b/crypto/kzg4844/kzg4844.go
@@ -34,9 +34,27 @@ var (
blobT = reflect.TypeFor[Blob]()
commitmentT = reflect.TypeFor[Commitment]()
proofT = reflect.TypeFor[Proof]()
+ cellT = reflect.TypeFor[Cell]()
)
-const CellProofsPerBlob = 128
+const (
+ CellProofsPerBlob = 128
+ CellsPerBlob = 128
+ DataPerBlob = 64
+)
+
+// Cell represents a single cell in a blob.
+type Cell [2048]byte
+
+// UnmarshalJSON parses a cell in hex syntax.
+func (c *Cell) UnmarshalJSON(input []byte) error {
+ return hexutil.UnmarshalFixedJSON(cellT, input, c[:])
+}
+
+// MarshalText returns the hex representation of c.
+func (c *Cell) MarshalText() ([]byte, error) {
+ return hexutil.Bytes(c[:]).MarshalText()
+}
// Blob represents a 4844 data blob.
type Blob [131072]byte
@@ -189,3 +207,75 @@ func CalcBlobHashV1(hasher hash.Hash, commit *Commitment) (vh [32]byte) {
func IsValidVersionedHash(h []byte) bool {
return len(h) == 32 && h[0] == 0x01
}
+
+// VerifyCells verifies a batch of proofs corresponding to the cells and blob commitments.
+//
+// For this function, it is sufficient to only provide some of the cells.
+//
+// The `cellIndices` specify which of the 128 cells of each blob are given.
+// Indices must be given in ascending order.
+//
+// Note the list of indices is shared among all blobs, i.e. for a given list of indices
+// [1, 2, 13], the cells slice must contain cells [1, 2, 13] of each blob.
+// Thus, `len(cells)` must be a multiple of `len(cellIndices)`.
+//
+// One proof must be given for each cell. As such, `len(proofs)` must equal `len(cells)`.
+func VerifyCells(cells []Cell, commitments []Commitment, proofs []Proof, cellIndices []uint64) error {
+ // commitments/proofs/cells validation
+ switch {
+ case len(commitments) == 0:
+ return errors.New("no commitments")
+ case len(proofs)%len(commitments) != 0:
+ return errors.New("len(proofs) must be a multiple of len(commitments)")
+ case len(cells) != len(proofs):
+ return errors.New("mismatched len(cellProofs) and len(cells)")
+ }
+ if err := validateCellIndices(cells, cellIndices); err != nil {
+ return err
+ }
+ if len(cells)/len(cellIndices) != len(commitments) {
+ return errors.New("invalid number of cells for blob count")
+ }
+
+ if useCKZG.Load() {
+ return ckzgVerifyCells(cells, commitments, proofs, cellIndices)
+ }
+ return gokzgVerifyCells(cells, commitments, proofs, cellIndices)
+}
+
+// ComputeCells computes the cells from the given blobs.
+func ComputeCells(blobs []Blob) ([]Cell, error) {
+ if useCKZG.Load() {
+ return ckzgComputeCells(blobs)
+ }
+ return gokzgComputeCells(blobs)
+}
+
+// RecoverBlobs recovers blobs from the given cells and cell indices.
+// In order to successfully recover, at least DataPerBlob (64) cells must be provided.
+//
+// For the layout of cells and cellIndices, please see [VerifyCells].
+func RecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
+ if err := validateCellIndices(cells, cellIndices); err != nil {
+ return nil, err
+ }
+ if useCKZG.Load() {
+ return ckzgRecoverBlobs(cells, cellIndices)
+ }
+ return gokzgRecoverBlobs(cells, cellIndices)
+}
+
+func validateCellIndices(cells []Cell, cellIndices []uint64) error {
+ switch {
+ case len(cellIndices) == 0:
+ return errors.New("no cellIndices given")
+ case len(cellIndices) > len(cells):
+ return errors.New("less cells than cellIndices")
+ case len(cellIndices) > CellsPerBlob:
+ return errors.New("too many cellIndices")
+ case len(cells)%len(cellIndices) != 0:
+ return errors.New("len(cells) must be a multiple of len(cellIndices)")
+ }
+ // The library checks the canonical ordering of indices, so we don't have to do it here.
+ return nil
+}
diff --git a/crypto/kzg4844/kzg4844_ckzg_cgo.go b/crypto/kzg4844/kzg4844_ckzg_cgo.go
index 93d5f4ff94..bacfa7095a 100644
--- a/crypto/kzg4844/kzg4844_ckzg_cgo.go
+++ b/crypto/kzg4844/kzg4844_ckzg_cgo.go
@@ -190,3 +190,92 @@ func ckzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProofs
}
return nil
}
+
+// ckzgVerifyCells verifies that the cell data corresponds to the provided commitments.
+func ckzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
+ ckzgIniter.Do(ckzgInit)
+ var (
+ proofs = make([]ckzg4844.Bytes48, len(cellProofs))
+ commits = make([]ckzg4844.Bytes48, 0, len(cellProofs))
+ indices = make([]uint64, 0, len(cellProofs))
+ kzgcells = make([]ckzg4844.Cell, 0, len(cellProofs))
+ )
+ for i := range cellProofs {
+ proofs[i] = (ckzg4844.Bytes48)(cellProofs[i])
+ kzgcells = append(kzgcells, (ckzg4844.Cell)(cells[i]))
+ }
+ if len(cellProofs)%len(commitments) != 0 {
+ return errors.New("wrong cell proofs and commitments length")
+ }
+ cellCounts := len(cellProofs) / len(commitments)
+ for _, commitment := range commitments {
+ for j := 0; j < cellCounts; j++ {
+ commits = append(commits, (ckzg4844.Bytes48)(commitment))
+ }
+ }
+ for j := 0; j < len(commitments); j++ {
+ indices = append(indices, cellIndices...)
+ }
+
+ valid, err := ckzg4844.VerifyCellKZGProofBatch(commits, indices, kzgcells, proofs)
+ if err != nil {
+ return err
+ }
+ if !valid {
+ return errors.New("invalid proof")
+ }
+ return nil
+}
+
+// ckzgComputeCells computes cells from blobs.
+func ckzgComputeCells(blobs []Blob) ([]Cell, error) {
+ ckzgIniter.Do(ckzgInit)
+ cells := make([]Cell, 0, ckzg4844.CellsPerExtBlob*len(blobs))
+
+ for i := range blobs {
+ cellsI, err := ckzg4844.ComputeCells((*ckzg4844.Blob)(&blobs[i]))
+ if err != nil {
+ return []Cell{}, err
+ }
+ for _, c := range cellsI {
+ cells = append(cells, Cell(c))
+ }
+ }
+ return cells, nil
+}
+
+// ckzgRecoverBlobs recovers blobs from cells and cell indices.
+func ckzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
+ ckzgIniter.Do(ckzgInit)
+
+ if len(cellIndices) == 0 || len(cells)%len(cellIndices) != 0 {
+ return []Blob{}, errors.New("cells with wrong length")
+ }
+
+ blobCount := len(cells) / len(cellIndices)
+ blobs := make([]Blob, 0, blobCount)
+
+ offset := 0
+ for range blobCount {
+ kzgcells := make([]ckzg4844.Cell, 0, len(cellIndices))
+
+ for _, cell := range cells[offset : offset+len(cellIndices)] {
+ kzgcells = append(kzgcells, ckzg4844.Cell(cell))
+ }
+
+ extCells, err := ckzg4844.RecoverCells(cellIndices, kzgcells)
+ if err != nil {
+ return []Blob{}, err
+ }
+
+ var blob Blob
+ for i, cell := range extCells[:DataPerBlob] {
+ copy(blob[i*len(cell):], cell[:])
+ }
+ blobs = append(blobs, blob)
+
+ offset = offset + len(cellIndices)
+ }
+
+ return blobs, nil
+}
diff --git a/crypto/kzg4844/kzg4844_ckzg_nocgo.go b/crypto/kzg4844/kzg4844_ckzg_nocgo.go
index 7c552e9a18..e1a3c0af1e 100644
--- a/crypto/kzg4844/kzg4844_ckzg_nocgo.go
+++ b/crypto/kzg4844/kzg4844_ckzg_nocgo.go
@@ -73,3 +73,15 @@ func ckzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, proof []Pr
func ckzgComputeCellProofs(blob *Blob) ([]Proof, error) {
panic("unsupported platform")
}
+
+func ckzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
+ panic("unsupported platform")
+}
+
+func ckzgComputeCells(blobs []Blob) ([]Cell, error) {
+ panic("unsupported platform")
+}
+
+func ckzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
+ panic("unsupported platform")
+}
diff --git a/crypto/kzg4844/kzg4844_gokzg.go b/crypto/kzg4844/kzg4844_gokzg.go
index 03627ebafb..ca45a6a560 100644
--- a/crypto/kzg4844/kzg4844_gokzg.go
+++ b/crypto/kzg4844/kzg4844_gokzg.go
@@ -148,3 +148,85 @@ func gokzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProof
}
return context.VerifyCellKZGProofBatch(commits, cellIndices, cells[:], proofs)
}
+
+// gokzgVerifyCells verifies that the cell data corresponds to the provided commitment.
+func gokzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
+ gokzgIniter.Do(gokzgInit)
+
+ var (
+ proofs = make([]gokzg4844.KZGProof, len(cellProofs))
+ commits = make([]gokzg4844.KZGCommitment, 0, len(cellProofs))
+ indices = make([]uint64, 0, len(cellProofs))
+ kzgcells = make([]*gokzg4844.Cell, 0, len(cellProofs))
+ )
+ // Copy over the cell proofs and cells
+ for i := range cellProofs {
+ proofs[i] = gokzg4844.KZGProof(cellProofs[i])
+ gc := gokzg4844.Cell(cells[i])
+ kzgcells = append(kzgcells, &gc)
+ }
+ cellCounts := len(cellProofs) / len(commitments)
+ // Blow up the commitments to be the same length as the proofs
+ for _, commitment := range commitments {
+ for j := 0; j < cellCounts; j++ {
+ commits = append(commits, gokzg4844.KZGCommitment(commitment))
+ }
+ }
+ for j := 0; j < len(commitments); j++ {
+ indices = append(indices, cellIndices...)
+ }
+
+ return context.VerifyCellKZGProofBatch(commits, indices, kzgcells, proofs)
+}
+
+// gokzgComputeCells computes cells from blobs.
+func gokzgComputeCells(blobs []Blob) ([]Cell, error) {
+ gokzgIniter.Do(gokzgInit)
+ cells := make([]Cell, 0, gokzg4844.CellsPerExtBlob*len(blobs))
+
+ for i := range blobs {
+ cellsI, err := context.ComputeCells((*gokzg4844.Blob)(&blobs[i]), 2)
+ if err != nil {
+ return []Cell{}, err
+ }
+ for _, c := range cellsI {
+ if c != nil {
+ cells = append(cells, Cell(*c))
+ }
+ }
+ }
+ return cells, nil
+}
+
+// gokzgRecoverBlobs recovers blobs from cells and cell indices.
+func gokzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
+ gokzgIniter.Do(gokzgInit)
+
+ blobCount := len(cells) / len(cellIndices)
+ blobs := make([]Blob, 0, blobCount)
+
+ offset := 0
+ for range blobCount {
+ kzgcells := make([]*gokzg4844.Cell, 0, len(cellIndices))
+
+ for _, cell := range cells[offset : offset+len(cellIndices)] {
+ gc := gokzg4844.Cell(cell)
+ kzgcells = append(kzgcells, &gc)
+ }
+
+ extCells, err := context.RecoverCells(cellIndices, kzgcells, 2)
+ if err != nil {
+ return []Blob{}, err
+ }
+
+ var blob Blob
+ for i, cell := range extCells[:DataPerBlob] {
+ copy(blob[i*len(cell):], cell[:])
+ }
+ blobs = append(blobs, blob)
+
+ offset = offset + len(cellIndices)
+ }
+
+ return blobs, nil
+}
diff --git a/crypto/kzg4844/kzg4844_test.go b/crypto/kzg4844/kzg4844_test.go
index 743a277199..056decfb8b 100644
--- a/crypto/kzg4844/kzg4844_test.go
+++ b/crypto/kzg4844/kzg4844_test.go
@@ -18,6 +18,8 @@ package kzg4844
import (
"crypto/rand"
+ mrand "math/rand"
+ "slices"
"testing"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
@@ -45,14 +47,20 @@ func randBlob() *Blob {
return &blob
}
-func TestCKZGWithPoint(t *testing.T) { testKZGWithPoint(t, true) }
-func TestGoKZGWithPoint(t *testing.T) { testKZGWithPoint(t, false) }
-func testKZGWithPoint(t *testing.T, ckzg bool) {
+func switchBackend(t testing.TB, ckzg bool) (switchBack func()) {
+ t.Helper()
if ckzg && !ckzgAvailable {
t.Skip("CKZG unavailable in this test build")
}
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
+ prev := useCKZG.Load()
useCKZG.Store(ckzg)
+ return func() { useCKZG.Store(prev) }
+}
+
+func TestCKZGWithPoint(t *testing.T) { testKZGWithPoint(t, true) }
+func TestGoKZGWithPoint(t *testing.T) { testKZGWithPoint(t, false) }
+func testKZGWithPoint(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
blob := randBlob()
@@ -73,11 +81,7 @@ func testKZGWithPoint(t *testing.T, ckzg bool) {
func TestCKZGWithBlob(t *testing.T) { testKZGWithBlob(t, true) }
func TestGoKZGWithBlob(t *testing.T) { testKZGWithBlob(t, false) }
func testKZGWithBlob(t *testing.T, ckzg bool) {
- if ckzg && !ckzgAvailable {
- t.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(t, ckzg)()
blob := randBlob()
@@ -97,11 +101,7 @@ func testKZGWithBlob(t *testing.T, ckzg bool) {
func BenchmarkCKZGBlobToCommitment(b *testing.B) { benchmarkBlobToCommitment(b, true) }
func BenchmarkGoKZGBlobToCommitment(b *testing.B) { benchmarkBlobToCommitment(b, false) }
func benchmarkBlobToCommitment(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
blob := randBlob()
@@ -113,11 +113,7 @@ func benchmarkBlobToCommitment(b *testing.B, ckzg bool) {
func BenchmarkCKZGComputeProof(b *testing.B) { benchmarkComputeProof(b, true) }
func BenchmarkGoKZGComputeProof(b *testing.B) { benchmarkComputeProof(b, false) }
func benchmarkComputeProof(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@@ -132,11 +128,7 @@ func benchmarkComputeProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGVerifyProof(b *testing.B) { benchmarkVerifyProof(b, true) }
func BenchmarkGoKZGVerifyProof(b *testing.B) { benchmarkVerifyProof(b, false) }
func benchmarkVerifyProof(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@@ -153,11 +145,7 @@ func benchmarkVerifyProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGComputeBlobProof(b *testing.B) { benchmarkComputeBlobProof(b, true) }
func BenchmarkGoKZGComputeBlobProof(b *testing.B) { benchmarkComputeBlobProof(b, false) }
func benchmarkComputeBlobProof(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@@ -172,11 +160,7 @@ func benchmarkComputeBlobProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGVerifyBlobProof(b *testing.B) { benchmarkVerifyBlobProof(b, true) }
func BenchmarkGoKZGVerifyBlobProof(b *testing.B) { benchmarkVerifyBlobProof(b, false) }
func benchmarkVerifyBlobProof(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@@ -192,11 +176,7 @@ func benchmarkVerifyBlobProof(b *testing.B, ckzg bool) {
func TestCKZGCells(t *testing.T) { testKZGCells(t, true) }
func TestGoKZGCells(t *testing.T) { testKZGCells(t, false) }
func testKZGCells(t *testing.T, ckzg bool) {
- if ckzg && !ckzgAvailable {
- t.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(t, ckzg)()
blob1 := randBlob()
blob2 := randBlob()
@@ -236,11 +216,7 @@ func BenchmarkGOKZGComputeCellProofs(b *testing.B) { benchmarkComputeCellProofs(
func BenchmarkCKZGComputeCellProofs(b *testing.B) { benchmarkComputeCellProofs(b, true) }
func benchmarkComputeCellProofs(b *testing.B, ckzg bool) {
- if ckzg && !ckzgAvailable {
- b.Skip("CKZG unavailable in this test build")
- }
- defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
- useCKZG.Store(ckzg)
+ defer switchBackend(b, ckzg)()
blob := randBlob()
_, _ = ComputeCellProofs(blob) // for kzg initialization
@@ -253,3 +229,208 @@ func benchmarkComputeCellProofs(b *testing.B, ckzg bool) {
}
}
}
+
+// randCellIndices picks n random unique indices from [0, CellsPerBlob) in sorted order.
+func randCellIndices(rng *mrand.Rand, n int) []uint64 {
+ perm := rng.Perm(CellsPerBlob)
+ indices := make([]uint64, n)
+ for i := 0; i < n; i++ {
+ indices[i] = uint64(perm[i])
+ }
+ slices.Sort(indices)
+ return indices
+}
+
+// randBlobAndProofs generates random blobs and precomputes their cells, proofs, and commitments.
+type randBlobAndProofs struct {
+ blobs []Blob
+ commitments []Commitment
+ cells []Cell // flat: blobs[i] cells at [i*CellsPerBlob : (i+1)*CellsPerBlob]
+ proofs []Proof
+}
+
+func newBlobs(t *testing.T, blobCount int) *randBlobAndProofs {
+ d := &randBlobAndProofs{
+ blobs: make([]Blob, blobCount),
+ commitments: make([]Commitment, blobCount),
+ }
+ for i := range blobCount {
+ d.blobs[i] = *randBlob()
+ commitment, err := BlobToCommitment(&d.blobs[i])
+ if err != nil {
+ t.Fatalf("failed to compute commitment: %v", err)
+ }
+ d.commitments[i] = commitment
+ proofs, err := ComputeCellProofs(&d.blobs[i])
+ if err != nil {
+ t.Fatalf("failed to compute cell proofs: %v", err)
+ }
+ d.proofs = append(d.proofs, proofs...)
+ }
+ cells, err := ComputeCells(d.blobs)
+ if err != nil {
+ t.Fatalf("failed to compute cells: %v", err)
+ }
+ d.cells = cells
+ return d
+}
+
+func TestCKZGVerifyPartialCells(t *testing.T) { testVerifyPartialCells(t, true) }
+func TestGoKZGVerifyPartialCells(t *testing.T) { testVerifyPartialCells(t, false) }
+
+func testVerifyPartialCells(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
+
+ const (
+ iterations = 50
+ blobCount = 3
+ cellsCount = 8
+ )
+ // Precompute blobs once, vary only cell indices per iteration
+ d := newBlobs(t, blobCount)
+
+ for iter := range iterations {
+ rng := mrand.New(mrand.NewSource(int64(iter)))
+ indices := randCellIndices(rng, cellsCount)
+
+ var partialCells []Cell
+ var partialProofs []Proof
+ for i := range blobCount {
+ for _, idx := range indices {
+ partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
+ partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
+ }
+ }
+ if err := VerifyCells(partialCells, d.commitments, partialProofs, indices); err != nil {
+ t.Fatalf("iter %d: failed to verify partial cells: %v", iter, err)
+ }
+ }
+}
+
+func TestCKZGVerifyCellsWithCorruptedCells(t *testing.T) {
+ testVerifyCellsWithCorruptedCells(t, true)
+}
+func TestGoKZGVerifyCellsWithCorruptedCells(t *testing.T) {
+ testVerifyCellsWithCorruptedCells(t, false)
+}
+
+func testVerifyCellsWithCorruptedCells(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
+
+ const blobCount = 3
+ d := newBlobs(t, blobCount)
+ indices := []uint64{0, 15, 63, 64, 95, 100, 120, 127}
+
+ var partialCells []Cell
+ var partialProofs []Proof
+ for i := range blobCount {
+ for _, idx := range indices {
+ partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
+ partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
+ }
+ }
+ // Corrupt the first cell
+ corruptedCells := make([]Cell, len(partialCells))
+ copy(corruptedCells, partialCells)
+ corruptedCells[0][0] ^= 0xff
+
+ if err := VerifyCells(corruptedCells, d.commitments, partialProofs, indices); err == nil {
+ t.Fatal("expected verification failure with corrupted cell")
+ }
+}
+
+func TestCKZGVerifyCellsWithCorruptedProofs(t *testing.T) {
+ testVerifyCellsWithCorruptedProofs(t, true)
+}
+func TestGoKZGVerifyCellsWithCorruptedProofs(t *testing.T) {
+ testVerifyCellsWithCorruptedProofs(t, false)
+}
+
+func testVerifyCellsWithCorruptedProofs(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
+
+ const blobCount = 3
+ d := newBlobs(t, blobCount)
+ indices := []uint64{0, 15, 63, 64, 95, 100, 120, 127}
+
+ var partialCells []Cell
+ var partialProofs []Proof
+ for i := range blobCount {
+ for _, idx := range indices {
+ partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
+ partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
+ }
+ }
+ // Swap first and last proof
+ wrongProofs := make([]Proof, len(partialProofs))
+ copy(wrongProofs, partialProofs)
+ wrongProofs[0], wrongProofs[len(wrongProofs)-1] = wrongProofs[len(wrongProofs)-1], wrongProofs[0]
+
+ if err := VerifyCells(partialCells, d.commitments, wrongProofs, indices); err == nil {
+ t.Fatal("expected verification failure with swapped proofs")
+ }
+}
+
+func TestCKZGRecoverBlob(t *testing.T) { testRecoverBlob(t, true) }
+func TestGoKZGRecoverBlob(t *testing.T) { testRecoverBlob(t, false) }
+
+func testRecoverBlob(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
+
+ // Precompute blobs once, vary only cell indices per iteration
+ d := newBlobs(t, 3)
+
+ for iter := range 50 {
+ rng := mrand.New(mrand.NewSource(int64(iter)))
+ numCells := DataPerBlob + rng.Intn(CellsPerBlob-DataPerBlob+1)
+ indices := randCellIndices(rng, numCells)
+
+ var partialCells []Cell
+ for bi := range 3 {
+ for _, idx := range indices {
+ partialCells = append(partialCells, d.cells[bi*CellsPerBlob+int(idx)])
+ }
+ }
+ recovered, err := RecoverBlobs(partialCells, indices)
+ if err != nil {
+ t.Fatalf("iter %d: failed to recover blob with %d cells: %v", iter, numCells, err)
+ }
+ if err := VerifyCellProofs(recovered, d.commitments, d.proofs); err != nil {
+ t.Fatalf("iter %d: recovered blobs failed verification: %v", iter, err)
+ }
+ for i := range d.blobs {
+ if recovered[i] != d.blobs[i] {
+ t.Fatalf("iter %d: recovered blob %d does not match original", iter, i)
+ }
+ }
+ }
+}
+
+func TestCKZGRecoverBlobWithInsufficientCells(t *testing.T) {
+ testRecoverBlobWithInsufficientCells(t, true)
+}
+func TestGoKZGRecoverBlobWithInsufficientCells(t *testing.T) {
+ testRecoverBlobWithInsufficientCells(t, false)
+}
+
+func testRecoverBlobWithInsufficientCells(t *testing.T, ckzg bool) {
+ defer switchBackend(t, ckzg)()
+
+ const blobCount = 3
+ d := newBlobs(t, blobCount)
+
+ // Use DataPerBlob-1 cells (one short of minimum required)
+ indices := make([]uint64, DataPerBlob-1)
+ for i := range indices {
+ indices[i] = uint64(i)
+ }
+ var partialCells []Cell
+ for bi := range blobCount {
+ for _, idx := range indices {
+ partialCells = append(partialCells, d.cells[bi*CellsPerBlob+int(idx)])
+ }
+ }
+ if _, err := RecoverBlobs(partialCells, indices); err == nil {
+ t.Fatalf("expected error with only %d cells, got none", len(indices))
+ }
+}
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 a4e976b1b8..33fe4fe5d9 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -236,9 +236,9 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B
if header == nil {
return nil, nil, errors.New("header not found")
}
- stateDb, err := b.eth.BlockChain().StateAt(header.Root)
+ stateDb, err := b.eth.BlockChain().StateAt(header)
if err != nil {
- stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
+ stateDb, err = b.eth.BlockChain().HistoricState(header)
if err != nil {
return nil, nil, err
}
@@ -261,9 +261,9 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN
if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash {
return nil, nil, errors.New("hash is not currently canonical")
}
- stateDb, err := b.eth.BlockChain().StateAt(header.Root)
+ stateDb, err := b.eth.BlockChain().StateAt(header)
if err != nil {
- stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
+ stateDb, err = b.eth.BlockChain().HistoricState(header)
if err != nil {
return nil, nil, err
}
diff --git a/eth/api_debug.go b/eth/api_debug.go
index 5dd535e672..260e24c2ee 100644
--- a/eth/api_debug.go
+++ b/eth/api_debug.go
@@ -82,7 +82,7 @@ func (api *DebugAPI) DumpBlock(blockNr rpc.BlockNumber) (state.Dump, error) {
if header == nil {
return state.Dump{}, fmt.Errorf("block #%d not found", blockNr)
}
- stateDb, err := api.eth.BlockChain().StateAt(header.Root)
+ stateDb, err := api.eth.BlockChain().StateAt(header)
if err != nil {
return state.Dump{}, err
}
@@ -167,7 +167,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex
if header == nil {
return state.Dump{}, fmt.Errorf("block #%d not found", number)
}
- stateDb, err = api.eth.BlockChain().StateAt(header.Root)
+ stateDb, err = api.eth.BlockChain().StateAt(header)
if err != nil {
return state.Dump{}, err
}
@@ -177,7 +177,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex
if block == nil {
return state.Dump{}, fmt.Errorf("block %s not found", hash.Hex())
}
- stateDb, err = api.eth.BlockChain().StateAt(block.Root())
+ stateDb, err = api.eth.BlockChain().StateAt(block.Header())
if err != nil {
return state.Dump{}, err
}
diff --git a/eth/backend.go b/eth/backend.go
index e9bea59734..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)),
@@ -277,8 +275,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
if config.OverrideBPO2 != nil {
overrides.OverrideBPO2 = config.OverrideBPO2
}
- if config.OverrideVerkle != nil {
- overrides.OverrideVerkle = config.OverrideVerkle
+ if config.OverrideUBT != nil {
+ overrides.OverrideUBT = config.OverrideUBT
}
options.Overrides = &overrides
@@ -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 8a4aced04b..1def169ae0 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.Blob
return nil, engine.InvalidParams.With(err)
}
// Validate the blobs from the pool and assemble the response
+ filled := 0
res := make([]*engine.BlobAndProofV2, 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.Blob
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/catalyst/api_test.go b/eth/catalyst/api_test.go
index d126c362fe..1f38c4dd8a 100644
--- a/eth/catalyst/api_test.go
+++ b/eth/catalyst/api_test.go
@@ -299,7 +299,7 @@ func TestEth2NewBlock(t *testing.T) {
ethservice.BlockChain().SubscribeRemovedLogsEvent(rmLogsCh)
for i := 0; i < 10; i++ {
- statedb, _ := ethservice.BlockChain().StateAt(parent.Root())
+ statedb, _ := ethservice.BlockChain().StateAt(parent.Header())
nonce := statedb.GetNonce(testAddr)
tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
ethservice.TxPool().Add([]*types.Transaction{tx}, true)
@@ -478,7 +478,7 @@ func TestFullAPI(t *testing.T) {
)
callback := func(parent *types.Header) {
- statedb, _ := ethservice.BlockChain().StateAt(parent.Root)
+ statedb, _ := ethservice.BlockChain().StateAt(parent)
nonce := statedb.GetNonce(testAddr)
tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
ethservice.TxPool().Add([]*types.Transaction{tx}, false)
@@ -604,7 +604,7 @@ func TestNewPayloadOnInvalidChain(t *testing.T) {
logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00")
)
for i := 0; i < 10; i++ {
- statedb, _ := ethservice.BlockChain().StateAt(parent.Root)
+ statedb, _ := ethservice.BlockChain().StateAt(parent)
tx := types.MustSignNewTx(testKey, signer, &types.LegacyTx{
Nonce: statedb.GetNonce(testAddr),
Value: new(big.Int),
@@ -1263,7 +1263,7 @@ func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) {
// Each block, this callback will include two txs that generate body values like logs and requests.
callback := func(parent *types.Header) {
var (
- statedb, _ = ethservice.BlockChain().StateAt(parent.Root)
+ statedb, _ = ethservice.BlockChain().StateAt(parent)
// Create tx to trigger log generator.
tx1, _ = types.SignTx(types.NewContractCreation(statedb.GetNonce(testAddr), new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
// Create tx to trigger deposit generator.
diff --git a/eth/catalyst/api_testing.go b/eth/catalyst/api_testing.go
index 8586029468..2818d7f0bb 100644
--- a/eth/catalyst/api_testing.go
+++ b/eth/catalyst/api_testing.go
@@ -74,6 +74,7 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
Random: payloadAttributes.Random,
Withdrawals: payloadAttributes.Withdrawals,
BeaconRoot: payloadAttributes.BeaconRoot,
+ SlotNum: payloadAttributes.SlotNumber,
}
return api.eth.Miner().BuildTestingPayload(args, txs, buildEmpty, extra)
}
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 9280d455fb..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
}
@@ -96,6 +95,7 @@ func (dl *downloadTester) newPeer(id string, version uint, blocks []*types.Block
id: id,
chain: newTestBlockchain(blocks),
withholdBodies: make(map[common.Hash]struct{}),
+ dropped: make(chan error, 1),
}
dl.peers[id] = peer
@@ -121,8 +121,11 @@ func (dl *downloadTester) dropPeer(id string) {
type downloadTesterPeer struct {
dl *downloadTester
withholdBodies map[common.Hash]struct{}
+ corruptBodies bool // if set, the peer serves incorrect blocks
id string
chain *core.BlockChain
+
+ dropped chan error // signaled when res.Done receives an error
}
func unmarshalRlpHeaders(rlpdata []rlp.RawValue) []*types.Header {
@@ -236,6 +239,11 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et
txsHashes[i] = hash
uncleHashes[i] = types.CalcUncleHash(body.Uncles)
}
+ if dlp.corruptBodies {
+ for i := range txsHashes {
+ txsHashes[i] = common.Hash{0xff}
+ }
+ }
req := ð.Request{
Peer: dlp.id,
}
@@ -248,10 +256,16 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et
WithdrawalRoots: withdrawalHashes,
},
Time: 1,
- Done: make(chan error, 1), // Ignore the returned status
+ Done: make(chan error),
}
go func() {
sink <- res
+ if err := <-res.Done; err != nil {
+ select {
+ case dlp.dropped <- err:
+ default:
+ }
+ }
}()
return req, nil
}
@@ -704,3 +718,21 @@ func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) {
t.Fatalf("Failed to sync chain in three seconds")
}
}
+
+func TestInvalidBodyPeerDrop(t *testing.T) {
+ tester := newTester(t, FullSync)
+ defer tester.terminate()
+
+ chain := testChainBase.shorten(blockCacheMaxItems - 15)
+ peer := tester.newPeer("corrupt", eth.ETH69, chain.blocks[1:])
+ peer.corruptBodies = true
+
+ if err := tester.downloader.BeaconSync(chain.blocks[len(chain.blocks)-1].Header(), nil); err != nil {
+ t.Fatalf("failed to beacon-sync chain: %v", err)
+ }
+ select {
+ case <-peer.dropped:
+ case <-time.After(1 * time.Minute):
+ t.Fatal("peer was not dropped")
+ }
+}
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/fetchers_concurrent.go b/eth/downloader/fetchers_concurrent.go
index 9d8cd114c1..51bf3404bd 100644
--- a/eth/downloader/fetchers_concurrent.go
+++ b/eth/downloader/fetchers_concurrent.go
@@ -323,25 +323,32 @@ func (d *Downloader) concurrentFetch(queue typedQueue) error {
delete(pending, res.Req.Peer)
delete(stales, res.Req.Peer)
- // Signal the dispatcher that the round trip is done. We'll drop the
- // peer if the data turns out to be junk.
- res.Done <- nil
- res.Req.Close()
-
// If the peer was previously banned and failed to deliver its pack
// in a reasonable time frame, ignore its message.
- if peer := d.peers.Peer(res.Req.Peer); peer != nil {
- // Deliver the received chunk of data and check chain validity
- accepted, err := queue.deliver(peer, res)
- if errors.Is(err, errInvalidChain) {
- return err
- }
- // Unless a peer delivered something completely else than requested (usually
- // caused by a timed out request which came through in the end), set it to
- // idle. If the delivery's stale, the peer should have already been idled.
- if !errors.Is(err, errStaleDelivery) {
- queue.updateCapacity(peer, accepted, res.Time)
- }
+ peer := d.peers.Peer(res.Req.Peer)
+ if peer == nil {
+ res.Done <- nil
+ res.Req.Close()
+ continue
+ }
+ // Deliver the received chunk of data and check chain validity
+ accepted, err := queue.deliver(peer, res)
+ // Unless a peer delivered something completely else than requested (usually
+ // caused by a timed out request which came through in the end), set it to
+ // idle. If the delivery's stale, the peer should have already been idled.
+ if !errors.Is(err, errStaleDelivery) {
+ queue.updateCapacity(peer, accepted, res.Time)
+ }
+ res.Done <- validityErrorOfRequest(err)
+ res.Req.Close()
+
+ if errors.Is(err, errInvalidChain) {
+ // errInvalidChain is the signal that processing of items failed internally,
+ // even though the items were validly encoded.
+ //
+ // This can be due to invalid blocks, or a database error.
+ // The sync cycle should be aborted for such errors, so we return it here.
+ return err
}
case cont := <-queue.waker():
@@ -352,3 +359,11 @@ func (d *Downloader) concurrentFetch(queue typedQueue) error {
}
}
}
+
+// validityErrorOfRequest returns err if it is related to block validity, and nil otherwise.
+func validityErrorOfRequest(err error) error {
+ if errors.Is(err, errInvalidBody) || errors.Is(err, errInvalidReceipt) {
+ return err
+ }
+ return nil
+}
diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go
index c0cb9b174a..585906b8bd 100644
--- a/eth/downloader/queue.go
+++ b/eth/downloader/queue.go
@@ -671,10 +671,10 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header,
}
// Assemble each of the results with their headers and retrieved data parts
var (
- accepted int
- failure error
- i int
- hashes []common.Hash
+ accepted int
+ failure error
+ i int
+ foundStale bool
)
for _, header := range request.Headers {
// Short circuit assembly if no more fetch results are found
@@ -686,42 +686,41 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header,
failure = err
break
}
- hashes = append(hashes, header.Hash())
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 {
- // else: between here and above, some other peer filled this result,
+ // Between here and above, some other peer filled this result,
// or it was indeed a no-op. This should not happen, but if it does it's
// not something to panic about
log.Error("Delivery stale", "stale", stale, "number", header.Number.Uint64(), "err", err)
- failure = errStaleDelivery
+ foundStale = true
}
// Clean up a successful fetch
- delete(taskPool, hashes[accepted])
- accepted++
+ delete(taskPool, header.Hash())
}
resDropMeter.Mark(int64(results - accepted))
// Return all failed or missing fetches to the queue
- for _, header := range request.Headers[accepted:] {
+ for _, header := range request.Headers[i:] {
taskQueue.Push(header, -int64(header.Number.Uint64()))
}
// Wake up Results
if accepted > 0 {
q.active.Signal()
}
- if failure == nil {
- return accepted, nil
+ if failure != nil {
+ return accepted, failure
}
// If none of the data was good, it's a stale delivery
- if accepted > 0 {
- return accepted, fmt.Errorf("partial failure: %v", failure)
+ if foundStale {
+ return accepted, errStaleDelivery
}
- return accepted, fmt.Errorf("%w: %v", failure, errStaleDelivery)
+ return accepted, nil
}
// Prepare configures the result cache to allow accepting and caching inbound
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index 01aaaa751b..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.
@@ -200,8 +207,8 @@ type Config struct {
// OverrideBPO2 (TODO: remove after the fork)
OverrideBPO2 *uint64 `toml:",omitempty"`
- // OverrideVerkle (TODO: remove after the fork)
- OverrideVerkle *uint64 `toml:",omitempty"`
+ // OverrideUBT (TODO: remove after the fork)
+ OverrideUBT *uint64 `toml:",omitempty"`
// EIP-7966: eth_sendRawTransactionSync timeouts
TxSyncDefaultTimeout time.Duration `toml:",omitempty"`
diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go
index 6f94a409e5..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:"-"`
@@ -64,7 +65,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
OverrideOsaka *uint64 `toml:",omitempty"`
OverrideBPO1 *uint64 `toml:",omitempty"`
OverrideBPO2 *uint64 `toml:",omitempty"`
- OverrideVerkle *uint64 `toml:",omitempty"`
+ OverrideUBT *uint64 `toml:",omitempty"`
TxSyncDefaultTimeout time.Duration `toml:",omitempty"`
TxSyncMaxTimeout time.Duration `toml:",omitempty"`
RangeLimit uint64 `toml:",omitempty"`
@@ -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
@@ -117,7 +119,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.OverrideOsaka = c.OverrideOsaka
enc.OverrideBPO1 = c.OverrideBPO1
enc.OverrideBPO2 = c.OverrideBPO2
- enc.OverrideVerkle = c.OverrideVerkle
+ enc.OverrideUBT = c.OverrideUBT
enc.TxSyncDefaultTimeout = c.TxSyncDefaultTimeout
enc.TxSyncMaxTimeout = c.TxSyncMaxTimeout
enc.RangeLimit = c.RangeLimit
@@ -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:"-"`
@@ -174,7 +177,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
OverrideOsaka *uint64 `toml:",omitempty"`
OverrideBPO1 *uint64 `toml:",omitempty"`
OverrideBPO2 *uint64 `toml:",omitempty"`
- OverrideVerkle *uint64 `toml:",omitempty"`
+ OverrideUBT *uint64 `toml:",omitempty"`
TxSyncDefaultTimeout *time.Duration `toml:",omitempty"`
TxSyncMaxTimeout *time.Duration `toml:",omitempty"`
RangeLimit *uint64 `toml:",omitempty"`
@@ -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
}
@@ -324,8 +330,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.OverrideBPO2 != nil {
c.OverrideBPO2 = dec.OverrideBPO2
}
- if dec.OverrideVerkle != nil {
- c.OverrideVerkle = dec.OverrideVerkle
+ if dec.OverrideUBT != nil {
+ c.OverrideUBT = dec.OverrideUBT
}
if dec.TxSyncDefaultTimeout != nil {
c.TxSyncDefaultTimeout = *dec.TxSyncDefaultTimeout
diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go
index 5817dfbcf5..20621c531d 100644
--- a/eth/fetcher/tx_fetcher.go
+++ b/eth/fetcher/tx_fetcher.go
@@ -992,7 +992,7 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{},
return // continue in the for-each
}
var (
- hashes = make([]common.Hash, 0, maxTxRetrievals)
+ hashes []common.Hash
bytes uint64
)
f.forEachAnnounce(f.announces[peer], func(hash common.Hash, meta txMetadata) bool {
@@ -1009,6 +1009,9 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{},
f.alternates[hash] = f.announced[hash]
delete(f.announced, hash)
+ if hashes == nil {
+ hashes = make([]common.Hash, 0, maxTxRetrievals)
+ }
// Accumulate the hash and stop if the limit was reached
hashes = append(hashes, hash)
if len(hashes) >= maxTxRetrievals {
diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go
index ad6491fd93..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.
@@ -63,24 +63,24 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
}
// Cap the maximum gas allowance according to EIP-7825 if the estimation targets Osaka
- if hi > params.MaxTxGas {
- if opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time) {
- hi = params.MaxTxGas
- }
+ isOsaka := opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time)
+ isAmsterdam := opts.Config.IsAmsterdam(opts.Header.Number, opts.Header.Time)
+ if hi > params.MaxTxGas && isOsaka && !isAmsterdam {
+ hi = params.MaxTxGas
}
// 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)
@@ -244,6 +244,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio
evmContext.BlobBaseFee = new(big.Int)
}
evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true})
+ defer evm.Release()
// Monitor the outer context and interrupt the EVM upon cancellation. To avoid
// a dangling goroutine until the outer estimation finishes, create an internal
diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go
index 02a25bc4d8..e57c6e11c5 100644
--- a/eth/gasprice/gasprice_test.go
+++ b/eth/gasprice/gasprice_test.go
@@ -104,7 +104,7 @@ func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.
func (b *testBackend) Pending() (*types.Block, types.Receipts, *state.StateDB) {
if b.pending {
block := b.chain.GetBlockByNumber(testHead + 1)
- state, _ := b.chain.StateAt(block.Root())
+ state, _ := b.chain.StateAt(block.Header())
return block, b.chain.GetReceiptsByHash(block.Hash()), state
}
return nil, nil, nil
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/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go
index 3f6a43a059..b0522c20bb 100644
--- a/eth/protocols/snap/handler_test.go
+++ b/eth/protocols/snap/handler_test.go
@@ -31,18 +31,24 @@ import (
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/holiman/uint256"
)
func makeTestBAL(minSize int) *bal.BlockAccessList {
n := minSize/33 + 1 // 33 bytes per storage read slot in RLP
access := bal.AccountAccess{
Address: common.HexToAddress("0x01"),
- StorageReads: make([][32]byte, n),
+ StorageReads: make([]*uint256.Int, n),
}
+ // Use a full-width 32-byte value (top byte 0xff) so each slot still
+ // encodes to 33 RLP bytes regardless of the index.
for i := range access.StorageReads {
- binary.BigEndian.PutUint64(access.StorageReads[i][24:], uint64(i))
+ var b [32]byte
+ b[0] = 0xff
+ binary.BigEndian.PutUint64(b[24:], uint64(i))
+ access.StorageReads[i] = new(uint256.Int).SetBytes(b[:])
}
- return &bal.BlockAccessList{Accesses: []bal.AccountAccess{access}}
+ return &bal.BlockAccessList{access}
}
// getChainWithBALs creates a minimal test chain with BALs stored for each block.
diff --git a/eth/protocols/snap/sync_test.go b/eth/protocols/snap/sync_test.go
index b11ad4e78a..c506488e91 100644
--- a/eth/protocols/snap/sync_test.go
+++ b/eth/protocols/snap/sync_test.go
@@ -25,6 +25,7 @@ import (
mrand "math/rand"
"slices"
"sync"
+ "sync/atomic"
"testing"
"time"
@@ -142,10 +143,10 @@ type testPeer struct {
term func()
// counters
- nAccountRequests int
- nStorageRequests int
- nBytecodeRequests int
- nTrienodeRequests int
+ nAccountRequests atomic.Int64
+ nStorageRequests atomic.Int64
+ nBytecodeRequests atomic.Int64
+ nTrienodeRequests atomic.Int64
}
func newTestPeer(id string, t *testing.T, term func()) *testPeer {
@@ -179,25 +180,25 @@ func (t *testPeer) Stats() string {
Storage requests: %d
Bytecode requests: %d
Trienode requests: %d
-`, t.nAccountRequests, t.nStorageRequests, t.nBytecodeRequests, t.nTrienodeRequests)
+`, t.nAccountRequests.Load(), t.nStorageRequests.Load(), t.nBytecodeRequests.Load(), t.nTrienodeRequests.Load())
}
func (t *testPeer) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes int) error {
t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes))
- t.nAccountRequests++
+ t.nAccountRequests.Add(1)
go t.accountRequestHandler(t, id, root, origin, limit, bytes)
return nil
}
func (t *testPeer) RequestTrieNodes(id uint64, root common.Hash, count int, paths []TrieNodePathSet, bytes int) error {
t.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes))
- t.nTrienodeRequests++
+ t.nTrienodeRequests.Add(1)
go t.trieRequestHandler(t, id, root, paths, bytes)
return nil
}
func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes int) error {
- t.nStorageRequests++
+ t.nStorageRequests.Add(1)
if len(accounts) == 1 && origin != nil {
t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes))
} else {
@@ -208,7 +209,7 @@ func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []
}
func (t *testPeer) RequestByteCodes(id uint64, hashes []common.Hash, bytes int) error {
- t.nBytecodeRequests++
+ t.nBytecodeRequests.Add(1)
t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes))
go t.codeRequestHandler(t, id, hashes, bytes)
return nil
@@ -1901,7 +1902,7 @@ func testSyncAccountPerformance(t *testing.T, scheme string) {
// sync cycle starts. When popping the queue, we do not look it up again.
// Doing so would bring this number down to zero in this artificial testcase,
// but only add extra IO for no reason in practice.
- if have, want := src.nTrienodeRequests, 1; have != want {
+ if have, want := src.nTrienodeRequests.Load(), int64(1); have != want {
fmt.Print(src.Stats())
t.Errorf("trie node heal requests wrong, want %d, have %d", want, have)
}
diff --git a/eth/state_accessor.go b/eth/state_accessor.go
index 04aac321cb..284ddf4305 100644
--- a/eth/state_accessor.go
+++ b/eth/state_accessor.go
@@ -56,7 +56,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *st
// The state is available in live database, create a reference
// on top to prevent garbage collection and return a release
// function to deref it.
- if statedb, err = eth.blockchain.StateAt(block.Root()); err == nil {
+ if statedb, err = eth.blockchain.StateAt(block.Header()); err == nil {
eth.blockchain.TrieDB().Reference(block.Root(), common.Hash{})
return statedb, func() {
eth.blockchain.TrieDB().Dereference(block.Root())
@@ -182,11 +182,12 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *st
func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), error) {
// Check if the requested state is available in the live chain.
- statedb, err := eth.blockchain.StateAt(block.Root())
+ header := block.Header()
+ statedb, err := eth.blockchain.StateAt(header)
if err == nil {
return statedb, noopReleaser, nil
}
- statedb, err = eth.blockchain.HistoricState(block.Root())
+ statedb, err = eth.blockchain.HistoricState(header)
if err == nil {
return statedb, noopReleaser, nil
}
@@ -246,13 +247,11 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
// Insert parent beacon block root in the state as per EIP-4788.
context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil)
evm := vm.NewEVM(context, statedb, eth.blockchain.Config(), vm.Config{})
- 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)
- }
+ defer evm.Release()
+
+ // 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
}
@@ -266,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 53a09087e4..0df02388b3 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -372,13 +372,9 @@ 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
// tracing state of block next depends on the parent state and construction
@@ -493,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 {
@@ -516,26 +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{})
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if chainConfig.IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+ defer evm.Release()
+ // 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
@@ -546,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))
}
@@ -584,12 +579,10 @@ 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{})
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if api.backend.ChainConfig().IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+ defer evm.Release()
+
+ // 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
@@ -673,6 +666,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
var failed error
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{})
+ defer evm.Release()
txloop:
for i, tx := range txs {
@@ -687,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
@@ -756,14 +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{})
- if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
- core.ProcessBeaconBlockRoot(*beaconRoot, evm)
- }
- if chainConfig.IsPrague(block.Number(), block.Time()) {
- core.ProcessParentBlockHash(block.ParentHash(), evm)
- }
+ defer evm.Release()
+
+ // 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())
@@ -790,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)
@@ -800,11 +793,12 @@ 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)
}
_, err = core.ApplyMessage(evm, msg, nil)
+ evm.Release()
if writer != nil {
writer.Flush()
}
@@ -817,7 +811,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
}
// Finalize the state so any modifications are written to the trie
// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
- statedb.Finalise(evm.ChainConfig().IsEIP158(block.Number()))
+ statedb.Finalise(chainConfig.IsEIP158(block.Number()))
// If we've traced the transaction we were looking for, abort
if tx.Hash() == txHash {
@@ -999,6 +993,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
}
tracingStateDB := state.NewHookedState(statedb, tracer.Hooks)
evm := vm.NewEVM(vmctx, tracingStateDB, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
+ defer evm.Release()
if precompiles != nil {
evm.SetPrecompiles(precompiles)
}
@@ -1021,7 +1016,7 @@ 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)
if err != nil {
@@ -1086,8 +1081,8 @@ func overrideConfig(original *params.ChainConfig, override *params.ChainConfig)
copy.OsakaTime = timestamp
canon = false
}
- if timestamp := override.VerkleTime; timestamp != nil {
- copy.VerkleTime = timestamp
+ if timestamp := override.UBTTime; timestamp != nil {
+ copy.UBTTime = timestamp
canon = false
}
diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go
index ecf3c99c8f..0e62b9631d 100644
--- a/eth/tracers/api_test.go
+++ b/eth/tracers/api_test.go
@@ -152,7 +152,7 @@ func (b *testBackend) teardown() {
}
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) {
- statedb, err := b.chain.StateAt(block.Root())
+ statedb, err := b.chain.StateAt(block.Header())
if err != nil {
return nil, nil, errStateNotFound
}
diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
index bb1a3d9f18..692c5eb775 100644
--- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go
+++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go
@@ -162,7 +162,7 @@ func setupTestBlockchain(t *testing.T, genesis *core.Genesis, tx *types.Transact
if genesisBlock == nil {
t.Fatalf("failed to get genesis block")
}
- statedb, err := blockchain.StateAt(genesisBlock.Root())
+ statedb, err := blockchain.StateAt(genesisBlock.Header())
if err != nil {
t.Fatalf("failed to get state: %v", err)
}
diff --git a/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json b/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json
new file mode 100644
index 0000000000..e376a98946
--- /dev/null
+++ b/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json
@@ -0,0 +1,98 @@
+{
+ "genesis": {
+ "blobGasUsed": "0",
+ "difficulty": "0",
+ "excessBlobGas": "0",
+ "extraData": "0x",
+ "gasLimit": "11511229",
+ "hash": "0x455b93a512baa4ed5e117508b184a6bb03904b94d665ce38931728eca9cdd8fe",
+ "miner": "0x71562b71999873db5b286df957af199ec94617f7",
+ "mixHash": "0x042877c4fab9f022d29590ae83bad89d6181afb1d6e107619911ea52e5901364",
+ "nonce": "0x0000000000000000",
+ "number": "1",
+ "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "stateRoot": "0xc8688ad6433e6b9f4edeb82360d2b99c8e919f493a01cacbe7c4a97184f5d043",
+ "timestamp": "1775654796",
+ "alloc": {
+ "0x71562b71999873db5b286df957af199ec94617f7": {
+ "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffdb64910c3bf7",
+ "nonce": "1"
+ },
+ "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+ "balance": "0x0",
+ "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
+ "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
+ "nonce": "1"
+ }
+ },
+ "config": {
+ "chainId": 1337,
+ "homesteadBlock": 0,
+ "eip150Block": 0,
+ "eip155Block": 0,
+ "eip158Block": 0,
+ "byzantiumBlock": 0,
+ "constantinopleBlock": 0,
+ "petersburgBlock": 0,
+ "istanbulBlock": 0,
+ "muirGlacierBlock": 0,
+ "berlinBlock": 0,
+ "londonBlock": 0,
+ "arrowGlacierBlock": 0,
+ "grayGlacierBlock": 0,
+ "shanghaiTime": 0,
+ "cancunTime": 0,
+ "pragueTime": 0,
+ "terminalTotalDifficulty": 0,
+ "blobSchedule": {
+ "cancun": {
+ "target": 3,
+ "max": 6,
+ "baseFeeUpdateFraction": 3338477
+ },
+ "prague": {
+ "target": 6,
+ "max": 9,
+ "baseFeeUpdateFraction": 5007716
+ }
+ }
+ }
+ },
+ "context": {
+ "number": "2",
+ "difficulty": "0",
+ "timestamp": "1775654797",
+ "gasLimit": "11522469",
+ "miner": "0x71562b71999873db5b286df957af199ec94617f7",
+ "baseFeePerGas": "766499147"
+ },
+ "input": "0x04f8cd82053901843b9aca008477359400830186a09471562b71999873db5b286df957af199ec94617f78080c0f85ef85c8205399400000000000000000000000000000000000000000101a011fc0271f2566e7ebe5ddbff6d48ea97a19afa248452a392781096b7e3b89177a0020107ecefe99c90429b416fe4d1eead5a7fa253761e85cd7cdc7df6e5032d7f80a098495fb16c904f0b67b49afe868b28b0159c8df07522bed99ef6ff2cc2ac2935a048857a9c385d91735a9fdccabc66de7a5ea1897f523a5b9a352e281642a76e6b",
+ "tracerConfig": {
+ "diffMode": true
+ },
+ "result": {
+ "post": {
+ "0x71562b71999873db5b286df957af199ec94617f7": {
+ "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffc1bd12c85eb7",
+ "nonce": 2
+ },
+ "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+ "code": "0x",
+ "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
+ "nonce": 2
+ }
+ },
+ "pre": {
+ "0x71562b71999873db5b286df957af199ec94617f7": {
+ "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffdb64910c3bf7",
+ "nonce": 1
+ },
+ "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+ "balance": "0x0",
+ "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
+ "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
+ "nonce": 1
+ }
+ }
+ }
+}
diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go
index 694debcf98..6570d73575 100644
--- a/eth/tracers/js/tracer_test.go
+++ b/eth/tracers/js/tracer_test.go
@@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
gasLimit uint64 = 31000
startGas uint64 = 10000
value = uint256.NewInt(0)
- contract = vm.NewContract(common.Address{}, common.Address{}, value, startGas, nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil)
)
evm.SetTxContext(vmctx.txCtx)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0}
@@ -183,7 +183,7 @@ func TestHaltBetweenSteps(t *testing.T) {
t.Fatal(err)
}
scope := &vm.ScopeContext{
- Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil),
+ Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), vm.GasBudget{}, nil),
}
evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, &dummyStatedb{}, chainConfig, vm.Config{Tracer: tracer.Hooks})
evm.SetTxContext(vm.TxContext{GasPrice: uint256.NewInt(1)})
@@ -281,7 +281,7 @@ func TestEnterExit(t *testing.T) {
t.Fatal(err)
}
scope := &vm.ScopeContext{
- Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil),
+ Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), vm.GasBudget{}, nil),
}
tracer.OnEnter(1, byte(vm.CALL), scope.Contract.Caller(), scope.Contract.Address(), []byte{}, 1000, new(big.Int))
tracer.OnExit(1, []byte{}, 400, nil, false)
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/logger/logger_test.go b/eth/tracers/logger/logger_test.go
index 554a37aff1..decdf588e1 100644
--- a/eth/tracers/logger/logger_test.go
+++ b/eth/tracers/logger/logger_test.go
@@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) {
var (
logger = NewStructLogger(nil)
evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()})
- contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), 100000, nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil)
)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)}
var index common.Hash
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/gen_account_json.go b/eth/tracers/native/gen_account_json.go
index 5fec2648b7..9417536a23 100644
--- a/eth/tracers/native/gen_account_json.go
+++ b/eth/tracers/native/gen_account_json.go
@@ -16,14 +16,14 @@ var _ = (*accountMarshaling)(nil)
func (a account) MarshalJSON() ([]byte, error) {
type account struct {
Balance *hexutil.Big `json:"balance,omitempty"`
- Code hexutil.Bytes `json:"code,omitempty"`
+ Code *hexutil.Bytes `json:"code,omitempty"`
CodeHash *common.Hash `json:"codeHash,omitempty"`
Nonce uint64 `json:"nonce,omitempty"`
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
}
var enc account
enc.Balance = (*hexutil.Big)(a.Balance)
- enc.Code = a.Code
+ enc.Code = (*hexutil.Bytes)(a.Code)
enc.CodeHash = a.CodeHash
enc.Nonce = a.Nonce
enc.Storage = a.Storage
@@ -47,7 +47,7 @@ func (a *account) UnmarshalJSON(input []byte) error {
a.Balance = (*big.Int)(dec.Balance)
}
if dec.Code != nil {
- a.Code = *dec.Code
+ a.Code = (*[]byte)(dec.Code)
}
if dec.CodeHash != nil {
a.CodeHash = dec.CodeHash
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 159a91b310..7026cca7f3 100644
--- a/eth/tracers/native/prestate.go
+++ b/eth/tracers/native/prestate.go
@@ -44,21 +44,24 @@ func init() {
type stateMap = map[common.Address]*account
type account struct {
- Balance *big.Int `json:"balance,omitempty"`
- Code []byte `json:"code,omitempty"`
+ Balance *big.Int `json:"balance,omitempty"`
+ // Code is a pointer so omitempty can omit unchanged code (nil) while
+ // still emitting "0x" when code is cleared (e.g. EIP-7702 deauth).
+ Code *[]byte `json:"code,omitempty"`
CodeHash *common.Hash `json:"codeHash,omitempty"`
Nonce uint64 `json:"nonce,omitempty"`
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
- empty bool
+
+ empty bool
}
func (a *account) exists() bool {
- return a.Nonce > 0 || len(a.Code) > 0 || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.Sign() != 0)
+ return a.Nonce > 0 || (a.Code != nil && len(*a.Code) > 0) || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.Sign() != 0)
}
type accountMarshaling struct {
Balance *hexutil.Big
- Code hexutil.Bytes
+ Code *hexutil.Bytes
}
type prestateTracer struct {
@@ -68,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
}
@@ -237,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)
}
@@ -266,24 +272,28 @@ func (t *prestateTracer) processDiffState() {
modified = true
postAccount.Nonce = newNonce
}
- prevCodeHash := common.Hash{}
+ // Empty code hashes are excluded from the prestate, so default
+ // to EmptyCodeHash to match what GetCodeHash returns for codeless accounts.
+ prevCodeHash := types.EmptyCodeHash
if t.pre[addr].CodeHash != nil {
prevCodeHash = *t.pre[addr].CodeHash
}
- // Empty code hashes are excluded from the prestate. Normalize
- // the empty code hash to a zero hash to make it comparable.
- if newCodeHash == types.EmptyCodeHash {
- newCodeHash = common.Hash{}
- }
if newCodeHash != prevCodeHash {
modified = true
postAccount.CodeHash = &newCodeHash
}
if !t.config.DisableCode {
newCode := t.env.StateDB.GetCode(addr)
- if !bytes.Equal(newCode, t.pre[addr].Code) {
+ var prevCode []byte
+ if t.pre[addr].Code != nil {
+ prevCode = *t.pre[addr].Code
+ }
+ if !bytes.Equal(newCode, prevCode) {
modified = true
- postAccount.Code = newCode
+ if newCode == nil {
+ newCode = []byte{}
+ }
+ postAccount.Code = &newCode
}
}
@@ -323,10 +333,13 @@ func (t *prestateTracer) lookupAccount(addr common.Address) {
return
}
+ code := t.env.StateDB.GetCode(addr)
acc := &account{
Balance: t.env.StateDB.GetBalance(addr).ToBig(),
Nonce: t.env.StateDB.GetNonce(addr),
- Code: t.env.StateDB.GetCode(addr),
+ }
+ if len(code) > 0 {
+ acc.Code = &code
}
codeHash := t.env.StateDB.GetCodeHash(addr)
// If the code is empty, we don't need to store it in the prestate.
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/go.mod b/go.mod
index 37a2537dd0..17897a62c0 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
github.com/ethereum/c-kzg-4844/v2 v2.1.6
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab
+ github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a
github.com/fatih/color v1.16.0
github.com/ferranbt/fastssz v0.1.4
github.com/fsnotify/fsnotify v1.6.0
@@ -43,7 +44,6 @@ require (
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/jackpal/go-nat-pmp v1.0.2
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267
- github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb
github.com/klauspost/compress v1.17.8
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-colorable v0.1.13
@@ -62,19 +62,20 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/urfave/cli/v2 v2.27.5
go.opentelemetry.io/otel v1.40.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
go.uber.org/automaxprocs v1.5.2
go.uber.org/goleak v1.3.0
- golang.org/x/crypto v0.44.0
+ golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
- golang.org/x/sync v0.18.0
+ golang.org/x/sync v0.19.0
golang.org/x/sys v0.40.0
- golang.org/x/text v0.31.0
+ golang.org/x/text v0.33.0
golang.org/x/time v0.9.0
- golang.org/x/tools v0.38.0
+ golang.org/x/tools v0.40.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
@@ -84,13 +85,13 @@ require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
- google.golang.org/grpc v1.77.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
+ google.golang.org/grpc v1.78.0 // indirect
)
require (
@@ -161,8 +162,8 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
- golang.org/x/mod v0.29.0 // indirect
- golang.org/x/net v0.47.0 // indirect
+ golang.org/x/mod v0.31.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index c465603242..bad8a44cfd 100644
--- a/go.sum
+++ b/go.sum
@@ -117,6 +117,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn2
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
+github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a h1:eIFUceK3U/z9UV0D/kAI6cxA27eH7MPqt2ks7fbzj/k=
+github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a/go.mod h1:nABYy4hsKZpuN0mu0uybdjrIOuGb1eE7b1lci/ezUAo=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
@@ -196,8 +198,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
@@ -225,8 +227,6 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb h1:Ag83At00qa4FLkcdMgrwHVSakqky/eZczOlxd4q336E=
-github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8=
github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4=
github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -382,10 +382,12 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -408,16 +410,16 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
-golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -433,8 +435,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -443,8 +445,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -496,8 +498,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
@@ -509,8 +511,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
-golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -518,12 +520,12 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
-google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
-google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
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/era/onedb/iterator.go b/internal/era/onedb/iterator.go
index b80fbabbc5..21dc5acbe0 100644
--- a/internal/era/onedb/iterator.go
+++ b/internal/era/onedb/iterator.go
@@ -156,22 +156,22 @@ func (it *RawIterator) Next() bool {
var n int64
if it.Header, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedHeader, off); it.err != nil {
it.clear()
- return true
+ return false
}
off += n
if it.Body, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedBody, off); it.err != nil {
it.clear()
- return true
+ return false
}
off += n
if it.Receipts, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedReceipts, off); it.err != nil {
it.clear()
- return true
+ return false
}
off += n
if it.TotalDifficulty, _, it.err = it.e.s.ReaderAt(era.TypeTotalDifficulty, off); it.err != nil {
it.clear()
- return true
+ return false
}
it.next += 1
return true
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 149e12c5b8..6d38c6c7c8 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -734,6 +734,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)
@@ -775,6 +779,7 @@ func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *s
blockContext.BlobBaseFee = new(big.Int)
}
evm := b.GetEVM(ctx, state, header, vmConfig, blockContext)
+ defer evm.Release()
if precompiles != nil {
evm.SetPrecompiles(precompiles)
}
@@ -991,6 +996,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)
}
@@ -1390,6 +1398,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
evm.Context.BlobBaseFee = new(big.Int)
}
res, err := core.ApplyMessage(evm, msg, nil)
+ evm.Release()
if err != nil {
return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction(types.LegacyTxType).Hash(), err)
}
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index b010eeaa08..63e75bd3e3 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -572,7 +572,7 @@ func (b testBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.Bloc
if header == nil {
return nil, nil, errors.New("header not found")
}
- stateDb, err := b.chain.StateAt(header.Root)
+ stateDb, err := b.chain.StateAt(header)
return stateDb, header, err
}
func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) {
@@ -1315,6 +1315,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 +2680,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/simulate.go b/internal/ethapi/simulate.go
index eacb296132..fa2ff2c32b 100644
--- a/internal/ethapi/simulate.go
+++ b/internal/ethapi/simulate.go
@@ -312,17 +312,15 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
tracingStateDB = state.NewHookedState(sim.state, hooks)
}
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.IsVerkle(header.Number, header.Time) {
- core.ProcessParentBlockHash(header.ParentHash, evm)
- }
- if header.ParentBeaconRoot != nil {
- core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, evm)
- }
+ // Run pre-execution system calls
+ 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
@@ -342,7 +340,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 {
@@ -393,21 +391,9 @@ 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, 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)
@@ -416,13 +402,18 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
blockBody := &types.Body{
Transactions: txes,
- Withdrawals: *block.BlockOverrides.Withdrawals,
+ }
+ // 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}
- b, err := sim.b.Engine().FinalizeAndAssemble(ctx, chainHeadReader, header, sim.state, blockBody, receipts)
- if err != nil {
- return nil, nil, nil, err
- }
+
+ // Assemble the block
+ b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts)
+
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/telemetry/tracesetup/setup.go b/internal/telemetry/tracesetup/setup.go
index 444416dd26..08c5f739b6 100644
--- a/internal/telemetry/tracesetup/setup.go
+++ b/internal/telemetry/tracesetup/setup.go
@@ -30,6 +30,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
@@ -83,6 +84,14 @@ func SetupTelemetry(cfg node.OpenTelemetryConfig, stack *node.Node) error {
if err != nil {
return fmt.Errorf("invalid rpc tracing endpoint URL: %w", err)
}
+ // Build auth headers once, shared across transports.
+ var authHeaders map[string]string
+ if cfg.AuthUser != "" {
+ authHeaders = map[string]string{
+ "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(cfg.AuthUser+":"+cfg.AuthPassword)),
+ }
+ }
+
var exporter *otlptrace.Exporter
switch u.Scheme {
case "http", "https":
@@ -95,12 +104,24 @@ func SetupTelemetry(cfg node.OpenTelemetryConfig, stack *node.Node) error {
if u.Path != "" && u.Path != "/" {
opts = append(opts, otlptracehttp.WithURLPath(u.Path))
}
- if cfg.AuthUser != "" {
- opts = append(opts, otlptracehttp.WithHeaders(map[string]string{
- "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(cfg.AuthUser+":"+cfg.AuthPassword)),
- }))
+ if authHeaders != nil {
+ opts = append(opts, otlptracehttp.WithHeaders(authHeaders))
}
exporter = otlptracehttp.NewUnstarted(opts...)
+ case "grpc", "grpcs":
+ if u.Path != "" && u.Path != "/" {
+ return fmt.Errorf("gRPC endpoints do not support URL paths: %s", u.Path)
+ }
+ opts := []otlptracegrpc.Option{
+ otlptracegrpc.WithEndpoint(u.Host),
+ }
+ if u.Scheme == "grpc" {
+ opts = append(opts, otlptracegrpc.WithInsecure())
+ }
+ if authHeaders != nil {
+ opts = append(opts, otlptracegrpc.WithHeaders(authHeaders))
+ }
+ exporter = otlptracegrpc.NewUnstarted(opts...)
default:
return fmt.Errorf("unsupported telemetry url scheme: %s", u.Scheme)
}
diff --git a/log/handler_glog.go b/log/handler_glog.go
index 739f8c5b42..13afb62ca4 100644
--- a/log/handler_glog.go
+++ b/log/handler_glog.go
@@ -19,9 +19,7 @@ package log
import (
"context"
"errors"
- "fmt"
"log/slog"
- "maps"
"regexp"
"runtime"
"strconv"
@@ -38,22 +36,22 @@ var errVmoduleSyntax = errors.New("expect comma-separated list of filename=N")
// matches; and requesting backtraces at certain positions.
type GlogHandler struct {
origin slog.Handler // The origin handler this wraps
+ lock sync.Mutex // synchronizes writes to config
+ config atomic.Pointer[glogConfig]
+}
- level atomic.Int32 // Current log level, atomically accessible
- override atomic.Bool // Flag whether overrides are used, atomically accessible
-
- patterns []pattern // Current list of patterns to override with
- siteCache map[uintptr]slog.Level // Cache of callsite pattern evaluations
- location string // file:line location where to do a stackdump at
- lock sync.RWMutex // Lock protecting the override pattern list
+type glogConfig struct {
+ patterns []pattern
+ cache sync.Map
+ level slog.Level
}
// NewGlogHandler creates a new log handler with filtering functionality similar
// to Google's glog logger. The returned handler implements Handler.
-func NewGlogHandler(h slog.Handler) *GlogHandler {
- return &GlogHandler{
- origin: h,
- }
+func NewGlogHandler(origin slog.Handler) *GlogHandler {
+ h := &GlogHandler{origin: origin}
+ h.config.Store(new(glogConfig))
+ return h
}
// pattern contains a filter for the Vmodule option, holding a verbosity level
@@ -66,7 +64,12 @@ type pattern struct {
// Verbosity sets the glog verbosity ceiling. The verbosity of individual packages
// and source files can be raised using Vmodule.
func (h *GlogHandler) Verbosity(level slog.Level) {
- h.level.Store(int32(level))
+ h.lock.Lock()
+ defer h.lock.Unlock()
+
+ cfg := h.config.Load()
+ newcfg := &glogConfig{level: level, patterns: cfg.patterns}
+ h.config.Store(newcfg)
}
// Vmodule sets the glog verbosity pattern.
@@ -128,13 +131,13 @@ func (h *GlogHandler) Vmodule(ruleset string) error {
re, _ := regexp.Compile(matcher)
filter = append(filter, pattern{re, level})
}
+
// Swap out the vmodule pattern for the new filter system
h.lock.Lock()
- defer h.lock.Unlock()
-
- h.patterns = filter
- h.siteCache = make(map[uintptr]slog.Level)
- h.override.Store(len(filter) != 0)
+ cfg := h.config.Load()
+ newcfg := &glogConfig{level: cfg.level, patterns: filter}
+ h.config.Store(newcfg)
+ h.lock.Unlock()
return nil
}
@@ -142,30 +145,9 @@ func (h *GlogHandler) Vmodule(ruleset string) error {
// Enabled implements slog.Handler, reporting whether the handler handles records
// at the given level.
func (h *GlogHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
- // fast-track skipping logging if override not enabled and the provided verbosity is above configured
- return h.override.Load() || slog.Level(h.level.Load()) <= lvl
-}
-
-// WithAttrs implements slog.Handler, returning a new Handler whose attributes
-// consist of both the receiver's attributes and the arguments.
-func (h *GlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
- h.lock.RLock()
- siteCache := maps.Clone(h.siteCache)
- h.lock.RUnlock()
-
- patterns := []pattern{}
- patterns = append(patterns, h.patterns...)
-
- res := GlogHandler{
- origin: h.origin.WithAttrs(attrs),
- patterns: patterns,
- siteCache: siteCache,
- location: h.location,
- }
-
- res.level.Store(h.level.Load())
- res.override.Store(h.override.Load())
- return &res
+ // fast-track skipping logging if vmodule is not enabled or level too low
+ cfg := h.config.Load()
+ return len(cfg.patterns) > 0 || cfg.level <= lvl
}
// WithGroup implements slog.Handler, returning a new Handler with the given
@@ -178,37 +160,70 @@ func (h *GlogHandler) WithGroup(name string) slog.Handler {
// Handle implements slog.Handler, filtering a log record through the global,
// local and backtrace filters, finally emitting it if either allow it through.
-func (h *GlogHandler) Handle(_ context.Context, r slog.Record) error {
- // If the global log level allows, fast track logging
- if slog.Level(h.level.Load()) <= r.Level {
- return h.origin.Handle(context.Background(), r)
- }
+func (h *GlogHandler) Handle(ctx context.Context, r slog.Record) error {
+ return h.handle(ctx, r, h.origin)
+}
- // Check callsite cache for previously calculated log levels
- h.lock.RLock()
- lvl, ok := h.siteCache[r.PC]
- h.lock.RUnlock()
-
- // If we didn't cache the callsite yet, calculate it
- if !ok {
- h.lock.Lock()
+func (h *GlogHandler) handle(ctx context.Context, r slog.Record, origin slog.Handler) error {
+ cfg := h.config.Load()
+ var lvl slog.Level
+ cachedLvl, ok := cfg.cache.Load(r.PC)
+ if ok {
+ // Fast path: cache hit
+ lvl = cachedLvl.(slog.Level)
+ } else {
+ // Resolve the callsite file.
fs := runtime.CallersFrames([]uintptr{r.PC})
frame, _ := fs.Next()
-
- for _, rule := range h.patterns {
- if rule.pattern.MatchString(fmt.Sprintf("+%s", frame.File)) {
- h.siteCache[r.PC], lvl, ok = rule.level, rule.level, true
+ file := frame.File
+ // Match against patterns and cache the level applied at this callsite.
+ lvl = cfg.level // default: use global level
+ for _, rule := range cfg.patterns {
+ if rule.pattern.MatchString("+" + file) {
+ lvl = rule.level
}
}
- // If no rule matched, remember to drop log the next time
- if !ok {
- h.siteCache[r.PC] = 0
- }
- h.lock.Unlock()
+ cfg.cache.Store(r.PC, lvl)
}
+
+ // Handle the message.
if lvl <= r.Level {
- return h.origin.Handle(context.Background(), r)
+ return origin.Handle(ctx, r)
}
return nil
}
+
+// WithAttrs implements slog.Handler, returning a new Handler whose attributes
+// consist of both the receiver's attributes and the arguments.
+//
+// Note the handler created here will still listen to Verbosity and Vmodule settings
+// done on the original handler.
+func (h *GlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return &glogWithAttrs{base: h, origin: h.origin.WithAttrs(attrs)}
+}
+
+type glogWithAttrs struct {
+ base *GlogHandler
+ origin slog.Handler
+}
+
+func (wh *glogWithAttrs) Enabled(ctx context.Context, lvl slog.Level) bool {
+ return wh.base.Enabled(ctx, lvl)
+}
+
+func (wh *glogWithAttrs) Handle(ctx context.Context, r slog.Record) error {
+ return wh.base.handle(ctx, r, wh.origin)
+}
+
+func (wh *glogWithAttrs) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return &glogWithAttrs{base: wh.base, origin: wh.origin.WithAttrs(attrs)}
+}
+
+// WithGroup implements slog.Handler, returning a new Handler with the given
+// group appended to the receiver's existing groups.
+//
+// Note, this function is not implemented.
+func (wh *glogWithAttrs) WithGroup(name string) slog.Handler {
+ panic("not implemented")
+}
diff --git a/log/logger_test.go b/log/logger_test.go
index dae8497204..c585ddab66 100644
--- a/log/logger_test.go
+++ b/log/logger_test.go
@@ -33,6 +33,64 @@ func TestLoggingWithVmodule(t *testing.T) {
}
}
+// TestLoggingWithVmoduleDowngrade checks that vmodule can be downgraded.
+func TestLoggingWithVmoduleDowngrade(t *testing.T) {
+ out := new(bytes.Buffer)
+ glog := NewGlogHandler(NewTerminalHandlerWithLevel(out, LevelTrace, false))
+ glog.Verbosity(LevelTrace) // Allow all logs globally
+ logger := NewLogger(glog)
+
+ // This should appear (global level allows it)
+ logger.Info("before vmodule downgrade, this should be logged")
+ if !bytes.Contains(out.Bytes(), []byte("before vmodule downgrade")) {
+ t.Fatal("expected 'before vmodule downgrade' to be logged")
+ }
+ out.Reset()
+
+ // Downgrade this file to only allow Warn and above
+ glog.Vmodule("logger_test.go=2")
+
+ // Info should now be filtered out
+ logger.Info("after vmodule downgrade, this should be filtered")
+ if bytes.Contains(out.Bytes(), []byte("after vmodule downgrade, this should be filtered")) {
+ t.Fatal("expected 'after vmodule downgrade, this should be filtered' to NOT be logged after vmodule downgrade")
+ }
+
+ // Warn should still appear
+ logger.Warn("after vmodule downgrade, this should be logged")
+ if !bytes.Contains(out.Bytes(), []byte("after vmodule downgrade, this should be logged")) {
+ t.Fatal("expected 'should appear' to be logged")
+ }
+}
+
+// TestWithAttrsVerbosityChange checks that verbosity changes affect child loggers.
+func TestWithAttrsVerbosityChange(t *testing.T) {
+ out := new(bytes.Buffer)
+ glog := NewGlogHandler(NewTerminalHandlerWithLevel(out, LevelTrace, false))
+ glog.Verbosity(LevelInfo)
+
+ // Create a child logger with an extra attribute.
+ child := slog.New(glog.WithAttrs([]slog.Attr{slog.String("peer", "foo")}))
+
+ // Debug should be filtered at Info level.
+ child.Debug("this should be filtered")
+ if bytes.Contains(out.Bytes(), []byte("this should be filtered")) {
+ t.Fatal("expected debug message to be filtered at Info level")
+ }
+
+ // Change verbosity on the parent to allow Debug.
+ glog.Verbosity(LevelDebug)
+
+ // Child should pick up the new level and include its attributes.
+ child.Debug("this should be logged")
+ if !bytes.Contains(out.Bytes(), []byte("this should be logged")) {
+ t.Fatal("expected child logger to pick up verbosity change")
+ }
+ if !bytes.Contains(out.Bytes(), []byte("peer=foo")) {
+ t.Fatal("expected child logger to include WithAttrs attributes")
+ }
+}
+
func TestTerminalHandlerWithAttrs(t *testing.T) {
out := new(bytes.Buffer)
glog := NewGlogHandler(NewTerminalHandlerWithLevel(out, LevelTrace, false).WithAttrs([]slog.Attr{slog.String("baz", "bat")}))
diff --git a/metrics/sample.go b/metrics/sample.go
index dc8167809f..95092ffc3c 100644
--- a/metrics/sample.go
+++ b/metrics/sample.go
@@ -314,7 +314,7 @@ func (s *UniformSample) Clear() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.count = 0
- clear(s.values)
+ s.values = s.values[:0]
}
// Snapshot returns a read-only copy of the sample.
diff --git a/miner/miner_test.go b/miner/miner_test.go
index 13475a19b6..5411418b13 100644
--- a/miner/miner_test.go
+++ b/miner/miner_test.go
@@ -80,10 +80,14 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block
return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil))
}
-func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
+func (bc *testBlockChain) StateAt(header *types.Header) (*state.StateDB, error) {
return bc.statedb, nil
}
+func (bc *testBlockChain) Genesis() *types.Block {
+ return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil))
+}
+
func (bc *testBlockChain) HasState(root common.Hash) bool {
return bc.root == root
}
diff --git a/miner/payload_building.go b/miner/payload_building.go
index ccaabec373..db8126828a 100644
--- a/miner/payload_building.go
+++ b/miner/payload_building.go
@@ -360,5 +360,5 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
if res.err != nil {
return nil, res.err
}
- return engine.BlockToExecutableData(res.block, new(big.Int), res.sidecars, res.requests), nil
+ return engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
}
diff --git a/miner/worker.go b/miner/worker.go
index 39a61de318..1ecee96688 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -83,6 +83,9 @@ func (env *environment) txFitsSize(tx *types.Transaction) bool {
// discard terminates the background threads before discarding it.
func (env *environment) discard() {
env.state.StopPrefetcher()
+ if env.evm != nil {
+ env.evm.Release()
+ }
}
const (
@@ -164,7 +167,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}
@@ -183,7 +186,21 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
}
}
}
- body := types.Body{Transactions: work.txs, Withdrawals: genParam.withdrawals}
+ // Construct the block body, the withdrawal list should never be null
+ // if Shanghai has been activated.
+ body := types.Body{
+ Transactions: work.txs,
+ Withdrawals: genParam.withdrawals,
+ }
+ if !miner.chainConfig.IsShanghai(work.header.Number, work.header.Time) {
+ if body.Withdrawals != nil {
+ return &newPayloadResult{err: errors.New("unexpected withdrawals before shanghai")}
+ }
+ } else {
+ if body.Withdrawals == nil {
+ body.Withdrawals = make([]*types.Withdrawal, 0)
+ }
+ }
allLogs := make([]*types.Log, 0)
for _, r := range work.receipts {
@@ -191,31 +208,19 @@ 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, 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
}
+ // 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)
+ assembleSpanEnd(nil)
- block, err := miner.engine.FinalizeAndAssemble(ctx, miner.chain, work.header, work.state, &body, work.receipts)
- if err != nil {
- return &newPayloadResult{err: err}
- }
return &newPayloadResult{
block: block,
fees: totalFees(block, work.receipts),
@@ -312,19 +317,15 @@ 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
+ core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)
return env, nil
}
// makeEnv creates a new environment for the sealing block.
func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool) (*environment, error) {
// Retrieve the parent state to execute on top.
- state, err := miner.chain.StateAt(parent.Root)
+ state, err := miner.chain.StateAtForkBoundary(parent, header)
if err != nil {
return nil, err
}
@@ -413,6 +414,7 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*
func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error {
ctx, _, spanEnd := telemetry.StartSpan(ctx, "miner.commitTransactions")
defer spanEnd(nil)
+
isCancun := miner.chainConfig.IsCancun(env.header.Number, env.header.Time)
for {
// Check interruption signal and abort building if it's fired.
@@ -500,7 +502,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 {
@@ -529,6 +531,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl
func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int32, env *environment) (err error) {
ctx, span, spanEnd := telemetry.StartSpan(ctx, "miner.fillTransactions")
defer spanEnd(&err)
+
miner.confMu.RLock()
tip := miner.config.GasPrice
prio := miner.prio
@@ -544,7 +547,7 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3
if env.header.ExcessBlobGas != nil {
filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(miner.chainConfig, env.header))
}
- if miner.chainConfig.IsOsaka(env.header.Number, env.header.Time) {
+ if miner.chainConfig.IsOsaka(env.header.Number, env.header.Time) && !miner.chainConfig.IsAmsterdam(env.header.Number, env.header.Time) {
filter.GasLimitCap = params.MaxTxGas
}
filter.BlobTxs = false
@@ -598,10 +601,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/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/common.go b/p2p/discover/common.go
index 767cc23b92..5513afd54d 100644
--- a/p2p/discover/common.go
+++ b/p2p/discover/common.go
@@ -17,9 +17,11 @@
package discover
import (
+ "container/list"
"crypto/ecdsa"
crand "crypto/rand"
"encoding/binary"
+ "iter"
"math/rand"
"net"
"net/netip"
@@ -143,3 +145,16 @@ func (r *reseedingRandom) Shuffle(n int, swap func(i, j int)) {
defer r.mu.Unlock()
r.cur.Shuffle(n, swap)
}
+
+// iterList iterates over the elements of the given list.
+func iterList[T any](l *list.List) iter.Seq2[T, *list.Element] {
+ return func(yield func(T, *list.Element) bool) {
+ for el := l.Front(); el != nil; {
+ next := el.Next()
+ if !yield(el.Value.(T), el) {
+ return
+ }
+ el = next
+ }
+ }
+}
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 dd3f363f7e..ae8cbec3e2 100644
--- a/p2p/discover/v4_udp.go
+++ b/p2p/discover/v4_udp.go
@@ -446,16 +446,16 @@ func (t *UDPv4) loop() {
}
// Start the timer so it fires when the next pending reply has expired.
now := time.Now()
- for el := plist.Front(); el != nil; el = el.Next() {
- nextTimeout = el.Value.(*replyMatcher)
- if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout {
+ for p, el := range iterList[*replyMatcher](plist) {
+ nextTimeout = p
+ if dist := p.deadline.Sub(now); dist < 2*respTimeout {
timeout.Reset(dist)
return
}
// 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
@@ -478,8 +478,7 @@ func (t *UDPv4) loop() {
case r := <-t.gotreply:
var matched bool // whether any replyMatcher considered the reply acceptable.
- for el := plist.Front(); el != nil; el = el.Next() {
- p := el.Value.(*replyMatcher)
+ for p, el := range iterList[*replyMatcher](plist) {
if p.from == r.from && p.ptype == r.data.Kind() && p.ip == r.ip {
ok, requestDone := p.callback(r.data)
matched = matched || ok
@@ -499,8 +498,7 @@ func (t *UDPv4) loop() {
nextTimeout = nil
// Notify and remove callbacks whose deadline is in the past.
- for el := plist.Front(); el != nil; el = el.Next() {
- p := el.Value.(*replyMatcher)
+ for p, el := range iterList[*replyMatcher](plist) {
if now.After(p.deadline) || now.Equal(p.deadline) {
p.errc <- errTimeout
plist.Remove(el)
@@ -557,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/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go
index 9429fbaf0a..1c41941c70 100644
--- a/p2p/discover/v5_udp_test.go
+++ b/p2p/discover/v5_udp_test.go
@@ -937,6 +937,7 @@ func newUDPV5Test(t *testing.T) *udpV5Test {
PrivateKey: test.localkey,
Log: testlog.Logger(t, log.LvlTrace),
ValidSchemes: enode.ValidSchemesForTesting,
+ PingInterval: 1000 * time.Hour,
})
test.udp.codec = &testCodec{test: test, id: ln.ID()}
test.table = test.udp.tab
diff --git a/params/config.go b/params/config.go
index 197ed56f8a..17508cbf27 100644
--- a/params/config.go
+++ b/params/config.go
@@ -207,7 +207,7 @@ var (
CancunTime: nil,
PragueTime: nil,
OsakaTime: nil,
- VerkleTime: nil,
+ UBTTime: nil,
Ethash: new(EthashConfig),
Clique: nil,
}
@@ -263,7 +263,7 @@ var (
CancunTime: nil,
PragueTime: nil,
OsakaTime: nil,
- VerkleTime: nil,
+ UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: nil,
Clique: &CliqueConfig{Period: 0, Epoch: 30000},
@@ -293,7 +293,7 @@ var (
CancunTime: nil,
PragueTime: nil,
OsakaTime: nil,
- VerkleTime: nil,
+ UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: new(EthashConfig),
Clique: nil,
@@ -323,7 +323,7 @@ var (
CancunTime: newUint64(0),
PragueTime: newUint64(0),
OsakaTime: newUint64(0),
- VerkleTime: nil,
+ UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(0),
Ethash: new(EthashConfig),
Clique: nil,
@@ -358,7 +358,7 @@ var (
CancunTime: nil,
PragueTime: nil,
OsakaTime: nil,
- VerkleTime: nil,
+ UBTTime: nil,
TerminalTotalDifficulty: big.NewInt(math.MaxInt64),
Ethash: new(EthashConfig),
Clique: nil,
@@ -466,7 +466,7 @@ type ChainConfig struct {
BPO4Time *uint64 `json:"bpo4Time,omitempty"` // BPO4 switch time (nil = no fork, 0 = already on bpo4)
BPO5Time *uint64 `json:"bpo5Time,omitempty"` // BPO5 switch time (nil = no fork, 0 = already on bpo5)
AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` // Amsterdam switch time (nil = no fork, 0 = already on amsterdam)
- VerkleTime *uint64 `json:"verkleTime,omitempty"` // Verkle switch time (nil = no fork, 0 = already on verkle)
+ UBTTime *uint64 `json:"ubtTime,omitempty"` // UBT switch time (nil = no fork, 0 = already on UBT)
// TerminalTotalDifficulty is the amount of total difficulty reached by
// the network that triggers the consensus upgrade.
@@ -474,18 +474,18 @@ type ChainConfig struct {
DepositContractAddress common.Address `json:"depositContractAddress,omitempty"`
- // EnableVerkleAtGenesis is a flag that specifies whether the network uses
+ // EnableUBTAtGenesis is a flag that specifies whether the network uses
// the Verkle tree starting from the genesis block. If set to true, the
- // genesis state will be committed using the Verkle tree, eliminating the
- // need for any Verkle transition later.
+ // genesis state will be committed using the Binary tree, eliminating the
+ // need for any Binary transition later.
//
- // This is a temporary flag only for verkle devnet testing, where verkle is
+ // This is a temporary flag only for binary devnet testing, where binary is
// activated at genesis, and the configured activation date has already passed.
//
- // In production networks (mainnet and public testnets), verkle activation
+ // In production networks (mainnet and public testnets), binary activation
// always occurs after the genesis block, making this flag irrelevant in
// those cases.
- EnableVerkleAtGenesis bool `json:"enableVerkleAtGenesis,omitempty"`
+ EnableUBTAtGenesis bool `json:"enableUBTAtGenesis,omitempty"`
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
@@ -595,8 +595,8 @@ func (c *ChainConfig) String() string {
if c.AmsterdamTime != nil {
result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime)
}
- if c.VerkleTime != nil {
- result += fmt.Sprintf(", VerkleTime: %v", *c.VerkleTime)
+ if c.UBTTime != nil {
+ result += fmt.Sprintf(", UBTTime: %v", *c.UBTTime)
}
result += "}"
return result
@@ -690,8 +690,8 @@ func (c *ChainConfig) Description() string {
if c.AmsterdamTime != nil {
banner += fmt.Sprintf(" - Amsterdam: @%-10v blob: (%s)\n", *c.AmsterdamTime, c.BlobScheduleConfig.Amsterdam)
}
- if c.VerkleTime != nil {
- banner += fmt.Sprintf(" - Verkle: @%-10v blob: (%s)\n", *c.VerkleTime, c.BlobScheduleConfig.Verkle)
+ if c.UBTTime != nil {
+ banner += fmt.Sprintf(" - UBT: @%-10v blob: (%s)\n", *c.UBTTime, c.BlobScheduleConfig.UBT)
}
banner += fmt.Sprintf("\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n")
return banner
@@ -717,13 +717,13 @@ type BlobScheduleConfig struct {
Cancun *BlobConfig `json:"cancun,omitempty"`
Prague *BlobConfig `json:"prague,omitempty"`
Osaka *BlobConfig `json:"osaka,omitempty"`
- Verkle *BlobConfig `json:"verkle,omitempty"`
BPO1 *BlobConfig `json:"bpo1,omitempty"`
BPO2 *BlobConfig `json:"bpo2,omitempty"`
BPO3 *BlobConfig `json:"bpo3,omitempty"`
BPO4 *BlobConfig `json:"bpo4,omitempty"`
BPO5 *BlobConfig `json:"bpo5,omitempty"`
Amsterdam *BlobConfig `json:"amsterdam,omitempty"`
+ UBT *BlobConfig `json:"ubt,omitempty"`
}
// IsHomestead returns whether num is either equal to the homestead block or greater.
@@ -866,12 +866,12 @@ func (c *ChainConfig) IsAmsterdam(num *big.Int, time uint64) bool {
return c.IsLondon(num) && isTimestampForked(c.AmsterdamTime, time)
}
-// IsVerkle returns whether time is either equal to the Verkle fork time or greater.
-func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool {
- return c.IsLondon(num) && isTimestampForked(c.VerkleTime, time)
+// IsUBT returns whether time is either equal to the Verkle fork time or greater.
+func (c *ChainConfig) IsUBT(num *big.Int, time uint64) bool {
+ return c.IsLondon(num) && isTimestampForked(c.UBTTime, time)
}
-// IsVerkleGenesis checks whether the verkle fork is activated at the genesis block.
+// IsUBTGenesis checks whether the verkle fork is activated at the genesis block.
//
// Verkle mode is considered enabled if the verkle fork time is configured,
// regardless of whether the local time has surpassed the fork activation time.
@@ -881,13 +881,13 @@ func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool {
// In production networks (mainnet and public testnets), verkle activation
// always occurs after the genesis block, making this function irrelevant in
// those cases.
-func (c *ChainConfig) IsVerkleGenesis() bool {
- return c.EnableVerkleAtGenesis
+func (c *ChainConfig) IsUBTGenesis() bool {
+ return c.EnableUBTAtGenesis
}
// IsEIP4762 returns whether eip 4762 has been activated at given block.
func (c *ChainConfig) IsEIP4762(num *big.Int, time uint64) bool {
- return c.IsVerkle(num, time)
+ return c.IsUBT(num, time)
}
// CheckCompatible checks whether scheduled fork transitions have been imported
@@ -945,7 +945,7 @@ func (c *ChainConfig) CheckConfigForkOrder() error {
{name: "cancunTime", timestamp: c.CancunTime, optional: true},
{name: "pragueTime", timestamp: c.PragueTime, optional: true},
{name: "osakaTime", timestamp: c.OsakaTime, optional: true},
- {name: "verkleTime", timestamp: c.VerkleTime, optional: true},
+ {name: "ubtTime", timestamp: c.UBTTime, optional: true},
{name: "bpo1", timestamp: c.BPO1Time, optional: true},
{name: "bpo2", timestamp: c.BPO2Time, optional: true},
{name: "bpo3", timestamp: c.BPO3Time, optional: true},
@@ -1104,8 +1104,8 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int,
if isForkTimestampIncompatible(c.OsakaTime, newcfg.OsakaTime, headTimestamp) {
return newTimestampCompatError("Osaka fork timestamp", c.OsakaTime, newcfg.OsakaTime)
}
- if isForkTimestampIncompatible(c.VerkleTime, newcfg.VerkleTime, headTimestamp) {
- return newTimestampCompatError("Verkle fork timestamp", c.VerkleTime, newcfg.VerkleTime)
+ if isForkTimestampIncompatible(c.UBTTime, newcfg.UBTTime, headTimestamp) {
+ return newTimestampCompatError("UBT fork timestamp", c.UBTTime, newcfg.UBTTime)
}
if isForkTimestampIncompatible(c.BPO1Time, newcfg.BPO1Time, headTimestamp) {
return newTimestampCompatError("BPO1 fork timestamp", c.BPO1Time, newcfg.BPO1Time)
@@ -1380,14 +1380,14 @@ type Rules struct {
IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
IsBerlin, IsLondon bool
IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool
- IsAmsterdam, IsVerkle bool
+ IsAmsterdam, IsUBT bool
}
// Rules ensures c's ChainID is not nil.
func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules {
// disallow setting Merge out of order
isMerge = isMerge && c.IsLondon(num)
- isVerkle := isMerge && c.IsVerkle(num, timestamp)
+ isUBT := isMerge && c.IsUBT(num, timestamp)
return Rules{
IsHomestead: c.IsHomestead(num),
IsEIP150: c.IsEIP150(num),
@@ -1398,7 +1398,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
IsPetersburg: c.IsPetersburg(num),
IsIstanbul: c.IsIstanbul(num),
IsBerlin: c.IsBerlin(num),
- IsEIP2929: c.IsBerlin(num) && !isVerkle,
+ IsEIP2929: c.IsBerlin(num) && !isUBT,
IsLondon: c.IsLondon(num),
IsMerge: isMerge,
IsShanghai: isMerge && c.IsShanghai(num, timestamp),
@@ -1406,7 +1406,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
IsPrague: isMerge && c.IsPrague(num, timestamp),
IsOsaka: isMerge && c.IsOsaka(num, timestamp),
IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp),
- IsVerkle: isVerkle,
- IsEIP4762: isVerkle,
+ IsUBT: isUBT,
+ IsEIP4762: isUBT,
}
}
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 652574287c..9da275c486 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -96,6 +96,7 @@ const (
TxDataNonZeroGasEIP2028 uint64 = 16 // Per byte of non zero data attached to a transaction after EIP 2028 (part in Istanbul)
TxTokenPerNonZeroByte uint64 = 4 // Token cost per non-zero byte as specified by EIP-7623.
TxCostFloorPerToken uint64 = 10 // Cost floor per byte of data as specified by EIP-7623.
+ TxCostFloorPerToken7976 uint64 = 16 // Cost floor per byte of data as specified by EIP-7976.
TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list
TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list
TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702
diff --git a/rlp/rlpgen/gen_test.go b/rlp/rlpgen/gen_test.go
index 4bfb1b9d25..2e35ef933d 100644
--- a/rlp/rlpgen/gen_test.go
+++ b/rlp/rlpgen/gen_test.go
@@ -26,6 +26,7 @@ import (
"go/types"
"os"
"path/filepath"
+ "runtime"
"testing"
)
@@ -52,6 +53,9 @@ var tests = []string{"uints", "nil", "rawvalue", "optional", "bigint", "uint256"
func TestOutput(t *testing.T) {
for _, test := range tests {
t.Run(test, func(t *testing.T) {
+ if test == "pkgclash" && runtime.GOOS == "windows" {
+ t.Skip("source-based importer is pathologically slow on Windows/NTFS")
+ }
inputFile := filepath.Join("testdata", test+".in.txt")
outputFile := filepath.Join("testdata", test+".out.txt")
bctx, typ, err := loadTestSource(inputFile, "Test")
diff --git a/rpc/websocket.go b/rpc/websocket.go
index 543ff617ba..ec676b9caf 100644
--- a/rpc/websocket.go
+++ b/rpc/websocket.go
@@ -324,6 +324,16 @@ func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, readL
}
func (wc *websocketCodec) close() {
+ // Send a WebSocket Close frame before closing the underlying connection,
+ // so the server sees a clean 1000 (normal closure) instead of 1006 (abnormal).
+ wc.jsonCodec.encMu.Lock()
+ wc.conn.WriteControl(
+ websocket.CloseMessage,
+ websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
+ time.Now().Add(wsPingWriteTimeout),
+ )
+ wc.jsonCodec.encMu.Unlock()
+
wc.jsonCodec.close()
wc.wg.Wait()
}
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index 401f4fba07..ee12bb263e 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -454,7 +454,9 @@ func (typedData *TypedData) EncodeType(primaryType string) hexutil.Bytes {
buffer.WriteString(obj.Name)
buffer.WriteString(",")
}
- buffer.Truncate(buffer.Len() - 1)
+ if len(typedData.Types[dep]) > 0 {
+ buffer.Truncate(buffer.Len() - 1)
+ }
buffer.WriteString(")")
}
return buffer.Bytes()
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/block_test.go b/tests/block_test.go
index c718b304b6..0f087967bb 100644
--- a/tests/block_test.go
+++ b/tests/block_test.go
@@ -66,6 +66,12 @@ func TestBlockchain(t *testing.T) {
// This directory contains no test.
bt.skipLoad(`.*\.meta/.*`)
+ // Broken tests
+ bt.skipLoad(`RevertInCreateInInit`)
+ bt.skipLoad(`InitCollisionParis`)
+ bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
+ bt.skipLoad(`create2collisionStorageParis`)
+
bt.walk(t, blockTestDir, func(t *testing.T, name string, test *BlockTest) {
execBlockTest(t, bt, test)
})
@@ -85,6 +91,12 @@ func TestExecutionSpecBlocktests(t *testing.T) {
bt.skipLoad(".*prague/eip7251_consolidations/test_system_contract_deployment.json")
bt.skipLoad(".*prague/eip7002_el_triggerable_withdrawals/test_system_contract_deployment.json")
+ // Broken tests
+ bt.skipLoad(`RevertInCreateInInit`)
+ bt.skipLoad(`InitCollisionParis`)
+ bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
+ bt.skipLoad(`create2collisionStorageParis`)
+
bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) {
execBlockTest(t, bt, test)
})
diff --git a/tests/block_test_util.go b/tests/block_test_util.go
index 00411073e2..bece8ae610 100644
--- a/tests/block_test_util.go
+++ b/tests/block_test_util.go
@@ -126,10 +126,10 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
db = rawdb.NewMemoryDatabase()
tconf = &triedb.Config{
Preimages: true,
- IsVerkle: gspec.Config.VerkleTime != nil && *gspec.Config.VerkleTime <= gspec.Timestamp,
+ IsUBT: gspec.Config.UBTTime != nil && *gspec.Config.UBTTime <= gspec.Timestamp,
}
)
- if scheme == rawdb.PathScheme || tconf.IsVerkle {
+ if scheme == rawdb.PathScheme || tconf.IsUBT {
tconf.PathDB = pathdb.Defaults
} else {
tconf.HashDB = hashdb.Defaults
diff --git a/tests/init.go b/tests/init.go
index f115e427a5..3db988a993 100644
--- a/tests/init.go
+++ b/tests/init.go
@@ -774,7 +774,7 @@ var Forks = map[string]*params.ChainConfig{
MergeNetsplitBlock: big.NewInt(0),
TerminalTotalDifficulty: big.NewInt(0),
ShanghaiTime: u64(0),
- VerkleTime: u64(0),
+ UBTTime: u64(0),
},
}
diff --git a/tests/state_test.go b/tests/state_test.go
index f80bda4372..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) {
@@ -57,6 +56,11 @@ func initMatcher(st *testMatcher) {
// Broken tests:
// EOF is not part of cancun
st.skipLoad(`^stEOF/`)
+
+ st.skipLoad(`RevertInCreateInInit`)
+ st.skipLoad(`InitCollisionParis`)
+ st.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
+ st.skipLoad(`create2collisionStorageParis`)
}
func TestState(t *testing.T) {
@@ -92,6 +96,12 @@ func TestExecutionSpecState(t *testing.T) {
}
st := new(testMatcher)
+ // Broken tests
+ st.skipLoad(`RevertInCreateInInit`)
+ st.skipLoad(`InitCollisionParis`)
+ st.skipLoad(`dynamicAccountOverwriteEmpty_Paris`)
+ st.skipLoad(`create2collisionStorageParis`)
+
st.walk(t, executionSpecStateTestDir, func(t *testing.T, name string, test *StateTest) {
execStateTest(t, st, test)
})
@@ -301,7 +311,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
evm.SetTxContext(txContext)
// Create "contract" for sender to cache code analysis.
- sender := vm.NewContract(msg.From, msg.From, nil, 0, nil)
+ sender := vm.NewContract(msg.From, msg.From, nil, vm.GasBudget{}, nil)
var (
gasUsed uint64
@@ -315,8 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StartTimer()
start := time.Now()
+ initialGas := vm.NewGasBudget(msg.GasLimit)
+
// Execute the message.
- _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, msg.GasLimit, 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
@@ -325,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StopTimer()
elapsed += uint64(time.Since(start))
refund += state.StateDB.GetRefund()
- gasUsed += msg.GasLimit - leftOverGas
+ gasUsed += leftOverGas.Used(initialGas)
state.StateDB.RevertToSnapshot(snapshot)
}
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 1dd1bf6a04..e33e15fc8c 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -173,7 +173,7 @@ func GetChainConfig(forkString string) (baseConfig *params.ChainConfig, eips []i
}
for _, eip := range eipsStrings {
if eipNum, err := strconv.Atoi(eip); err != nil {
- return nil, nil, fmt.Errorf("syntax error, invalid eip number %v", eipNum)
+ return nil, nil, fmt.Errorf("syntax error, invalid eip number %v", eip)
} else {
if !vm.ValidEip(eipNum) {
return nil, nil, fmt.Errorf("syntax error, invalid eip number %v", eipNum)
@@ -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
@@ -544,7 +544,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo
}
snaps, _ = snapshot.New(snapconfig, db, triedb, root)
}
- sdb = state.NewDatabase(triedb, nil).WithSnapshot(snaps)
+ sdb = state.NewMPTDatabase(triedb, nil).WithSnapshot(snaps)
statedb, _ = state.New(root, sdb)
return StateTestState{statedb, triedb, snaps}
}
diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go
index a90c2d522f..572c109f1e 100644
--- a/tests/transaction_test_util.go
+++ b/tests/transaction_test_util.go
@@ -71,7 +71,7 @@ func (tt *TransactionTest) Run() error {
if err := tt.validate(); err != nil {
return err
}
- validateTx := func(rlpData hexutil.Bytes, signer types.Signer, rules *params.Rules) (sender common.Address, hash common.Hash, requiredGas uint64, err error) {
+ validateTx := func(rlpData hexutil.Bytes, signer types.Signer, rules params.Rules) (sender common.Address, hash common.Hash, requiredGas uint64, err error) {
tx := new(types.Transaction)
if err = tx.UnmarshalBinary(rlpData); err != nil {
return
@@ -81,17 +81,18 @@ func (tt *TransactionTest) Run() error {
return
}
// Intrinsic gas
- requiredGas, 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
}
+ requiredGas = cost.RegularGas
if requiredGas > tx.Gas() {
return sender, hash, 0, fmt.Errorf("insufficient gas ( %d < %d )", tx.Gas(), requiredGas)
}
if rules.IsPrague {
var floorDataGas uint64
- floorDataGas, err = core.FloorDataGas(tx.Data())
+ floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList())
if err != nil {
return
}
@@ -132,7 +133,7 @@ func (tt *TransactionTest) Run() error {
rules = config.Rules(new(big.Int), testcase.isMerge, 0)
signer = types.MakeSigner(config, new(big.Int), 0)
)
- sender, hash, gas, err := validateTx(tt.Txbytes, signer, &rules)
+ sender, hash, gas, err := validateTx(tt.Txbytes, signer, rules)
if err != nil {
if expected.Hash != nil {
return fmt.Errorf("unexpected error fork %s: %v", testcase.name, err)
diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go
index a7392ec958..3516bf6bd5 100644
--- a/trie/bintrie/binary_node.go
+++ b/trie/bintrie/binary_node.go
@@ -16,138 +16,43 @@
package bintrie
-import (
- "errors"
-
- "github.com/ethereum/go-ethereum/common"
-)
-
-type (
- NodeFlushFn func([]byte, BinaryNode)
- NodeResolverFn func([]byte, common.Hash) ([]byte, error)
-)
+import "github.com/ethereum/go-ethereum/common"
// zero is the zero value for a 32-byte array.
var zero [32]byte
const (
- StemNodeWidth = 256 // Number of child per leaf node
- StemSize = 31 // Number of bytes to travel before reaching a group of leaves
- NodeTypeBytes = 1 // Size of node type prefix in serialization
- HashSize = 32 // Size of a hash in bytes
- BitmapSize = 32 // Size of the bitmap in a stem node
+ StemNodeWidth = 256 // Number of children per leaf node
+ StemSize = 31 // Number of bytes to travel before reaching a group of leaves
+ 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 // Stem node, contains a stem and a bitmap of values
+ nodeTypeStem = iota + 1
nodeTypeInternal
)
-// BinaryNode is an interface for a binary trie node.
-type BinaryNode interface {
- Get([]byte, NodeResolverFn) ([]byte, error)
- Insert([]byte, []byte, NodeResolverFn, int) (BinaryNode, error)
- Copy() BinaryNode
- Hash() common.Hash
- GetValuesAtStem([]byte, NodeResolverFn) ([][]byte, error)
- InsertValuesAtStem([]byte, [][]byte, NodeResolverFn, int) (BinaryNode, error)
- CollectNodes([]byte, NodeFlushFn) error
-
- toDot(parent, path string) string
- GetHeight() int
-}
-
-// SerializeNode serializes a binary trie node into a byte slice.
-func SerializeNode(node BinaryNode) []byte {
- switch n := (node).(type) {
- case *InternalNode:
- // InternalNode: 1 byte type + 32 bytes left hash + 32 bytes right hash
- var serialized [NodeTypeBytes + HashSize + HashSize]byte
- serialized[0] = nodeTypeInternal
- copy(serialized[1:33], n.left.Hash().Bytes())
- copy(serialized[33:65], n.right.Hash().Bytes())
- return serialized[:]
- case *StemNode:
- // StemNode: 1 byte type + 31 bytes stem + 32 bytes bitmap + 256*32 bytes values
- var serialized [NodeTypeBytes + StemSize + BitmapSize + StemNodeWidth*HashSize]byte
- serialized[0] = nodeTypeStem
- copy(serialized[NodeTypeBytes:NodeTypeBytes+StemSize], n.Stem)
- bitmap := serialized[NodeTypeBytes+StemSize : NodeTypeBytes+StemSize+BitmapSize]
- offset := NodeTypeBytes + StemSize + BitmapSize
- for i, v := range n.Values {
- if v != nil {
- bitmap[i/8] |= 1 << (7 - (i % 8))
- copy(serialized[offset:offset+HashSize], v)
- offset += HashSize
- }
- }
- // Only return the actual data, not the entire array
- return serialized[:offset]
- default:
- panic("invalid node type")
+// DeserializeAndHash deserializes a node from bytes and returns its hash.
+// This is a convenience function for external callers that need to compute
+// the hash of a serialized node without maintaining a nodeStore.
+func DeserializeAndHash(blob []byte, depth int) (common.Hash, error) {
+ s := newNodeStore()
+ ref, err := s.deserializeNode(blob, depth)
+ if err != nil {
+ return common.Hash{}, err
}
-}
-
-var invalidSerializedLength = errors.New("invalid serialized node length")
-
-// DeserializeNode deserializes a binary trie node from a byte slice. The
-// hash will be recomputed from the deserialized data.
-func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) {
- return deserializeNode(serialized, depth, common.Hash{}, true)
-}
-
-// DeserializeNodeWithHash deserializes a binary trie node from a byte slice, using the provided hash.
-func DeserializeNodeWithHash(serialized []byte, depth int, hn common.Hash) (BinaryNode, error) {
- return deserializeNode(serialized, depth, hn, false)
-}
-
-func deserializeNode(serialized []byte, depth int, hn common.Hash, mustRecompute bool) (BinaryNode, error) {
- if len(serialized) == 0 {
- return Empty{}, nil
- }
-
- switch serialized[0] {
- case nodeTypeInternal:
- if len(serialized) != 65 {
- return nil, invalidSerializedLength
- }
- return &InternalNode{
- depth: depth,
- left: HashedNode(common.BytesToHash(serialized[1:33])),
- right: HashedNode(common.BytesToHash(serialized[33:65])),
- hash: hn,
- mustRecompute: mustRecompute,
- }, nil
- case nodeTypeStem:
- if len(serialized) < 64 {
- return nil, invalidSerializedLength
- }
- var values [StemNodeWidth][]byte
- bitmap := serialized[NodeTypeBytes+StemSize : NodeTypeBytes+StemSize+BitmapSize]
- offset := NodeTypeBytes + StemSize + BitmapSize
-
- for i := range StemNodeWidth {
- if bitmap[i/8]>>(7-(i%8))&1 == 1 {
- if len(serialized) < offset+HashSize {
- return nil, invalidSerializedLength
- }
- values[i] = serialized[offset : offset+HashSize]
- offset += HashSize
- }
- }
- return &StemNode{
- Stem: serialized[NodeTypeBytes : NodeTypeBytes+StemSize],
- Values: values[:],
- depth: depth,
- hash: hn,
- mustRecompute: mustRecompute,
- }, nil
- default:
- return nil, errors.New("invalid node type")
- }
-}
-
-// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
-func ToDot(root BinaryNode) string {
- return root.toDot("", "")
+ return s.computeHash(ref), nil
}
diff --git a/trie/bintrie/binary_node_test.go b/trie/bintrie/binary_node_test.go
index 242743ba53..857060a0c0 100644
--- a/trie/bintrie/binary_node_test.go
+++ b/trie/bintrie/binary_node_test.go
@@ -23,79 +23,108 @@ import (
"github.com/ethereum/go-ethereum/common"
)
-// TestSerializeDeserializeInternalNode tests serialization and deserialization of InternalNode
+// TestSerializeDeserializeInternalNode tests grouped serialization and
+// deserialization of InternalNode through nodeStore at groupDepth=1.
func TestSerializeDeserializeInternalNode(t *testing.T) {
- // Create an internal node with two hashed children
leftHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
rightHash := common.HexToHash("0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")
- node := &InternalNode{
- depth: 5,
- left: HashedNode(leftHash),
- right: HashedNode(rightHash),
- }
+ s := newNodeStore()
+ leftRef := s.newHashedRef(leftHash)
+ rightRef := s.newHashedRef(rightHash)
- // Serialize the node
- serialized := SerializeNode(node)
+ rootRef := s.newInternalRef(0)
+ rootNode := s.getInternal(rootRef.Index())
+ rootNode.left = leftRef
+ rootNode.right = rightRef
+ s.root = 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
if serialized[0] != nodeTypeInternal {
t.Errorf("Expected type byte to be %d, got %d", nodeTypeInternal, serialized[0])
}
-
- if len(serialized) != 65 {
- t.Errorf("Expected serialized length to be 65, got %d", len(serialized))
+ if serialized[1] != 1 {
+ t.Errorf("Expected groupDepth byte to be 1, got %d", serialized[1])
}
- // Deserialize the node
- deserialized, err := DeserializeNode(serialized, 5)
+ 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))
+ }
+
+ // 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[hashesStart+HashSize:], rightHash[:]) {
+ t.Error("Right hash not found at expected position")
+ }
+
+ // Deserialize into a new store
+ ds := newNodeStore()
+ deserialized, err := ds.deserializeNode(serialized, 0)
if err != nil {
t.Fatalf("Failed to deserialize node: %v", err)
}
- // Check that it's an internal node
- internalNode, ok := deserialized.(*InternalNode)
- if !ok {
- t.Fatalf("Expected InternalNode, got %T", deserialized)
+ // Root should be an InternalNode
+ if deserialized.Kind() != kindInternal {
+ t.Fatalf("Expected kindInternal, got kind %d", deserialized.Kind())
}
- // Check the depth
- if internalNode.depth != 5 {
- t.Errorf("Expected depth 5, got %d", internalNode.depth)
+ internalNode := ds.getInternal(deserialized.Index())
+ if internalNode.depth != 0 {
+ t.Errorf("Expected depth 0, got %d", internalNode.depth)
}
- // Check the left and right hashes
- if internalNode.left.Hash() != leftHash {
- t.Errorf("Left hash mismatch: expected %x, got %x", leftHash, internalNode.left.Hash())
+ // Left child should be a HashedNode with the correct hash
+ if internalNode.left.Kind() != kindHashed {
+ t.Fatalf("Expected left child to be kindHashed, got %d", internalNode.left.Kind())
+ }
+ if ds.computeHash(internalNode.left) != leftHash {
+ t.Errorf("Left hash mismatch: expected %x, got %x", leftHash, ds.computeHash(internalNode.left))
}
- if internalNode.right.Hash() != rightHash {
- t.Errorf("Right hash mismatch: expected %x, got %x", rightHash, internalNode.right.Hash())
+ // Right child should be a HashedNode with the correct hash
+ if internalNode.right.Kind() != kindHashed {
+ t.Fatalf("Expected right child to be kindHashed, got %d", internalNode.right.Kind())
+ }
+ if ds.computeHash(internalNode.right) != rightHash {
+ t.Errorf("Right hash mismatch: expected %x, got %x", rightHash, ds.computeHash(internalNode.right))
}
}
-// TestSerializeDeserializeStemNode tests serialization and deserialization of StemNode
+// TestSerializeDeserializeStemNode tests serialization and deserialization of StemNode through nodeStore.
func TestSerializeDeserializeStemNode(t *testing.T) {
- // Create a stem node with some values
stem := make([]byte, StemSize)
for i := range stem {
stem[i] = byte(i)
}
var values [StemNodeWidth][]byte
- // Add some values at different indices
values[0] = common.HexToHash("0x0101010101010101010101010101010101010101010101010101010101010101").Bytes()
values[10] = common.HexToHash("0x0202020202020202020202020202020202020202020202020202020202020202").Bytes()
values[255] = common.HexToHash("0x0303030303030303030303030303030303030303030303030303030303030303").Bytes()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 10,
+ s := newNodeStore()
+ ref := s.newStemRef(stem, 10)
+ sn := s.getStem(ref.Index())
+ for i, v := range values {
+ if v != nil {
+ sn.setValue(byte(i), v)
+ }
}
// Serialize the node
- serialized := SerializeNode(node)
+ serialized := s.serializeNode(ref, 8)
// Check the serialized format
if serialized[0] != nodeTypeStem {
@@ -107,31 +136,32 @@ func TestSerializeDeserializeStemNode(t *testing.T) {
t.Errorf("Stem mismatch in serialized data")
}
- // Deserialize the node
- deserialized, err := DeserializeNode(serialized, 10)
+ // Deserialize into a new store
+ ds := newNodeStore()
+ deserializedRef, err := ds.deserializeNode(serialized, 10)
if err != nil {
t.Fatalf("Failed to deserialize node: %v", err)
}
- // Check that it's a stem node
- stemNode, ok := deserialized.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", deserialized)
+ if deserializedRef.Kind() != kindStem {
+ t.Fatalf("Expected kindStem, got kind %d", deserializedRef.Kind())
}
+ stemNode := ds.getStem(deserializedRef.Index())
+
// Check the stem
- if !bytes.Equal(stemNode.Stem, stem) {
+ if !bytes.Equal(stemNode.Stem[:], stem) {
t.Errorf("Stem mismatch after deserialization")
}
// Check the values
- if !bytes.Equal(stemNode.Values[0], values[0]) {
+ if !bytes.Equal(stemNode.getValue(0), values[0]) {
t.Errorf("Value at index 0 mismatch")
}
- if !bytes.Equal(stemNode.Values[10], values[10]) {
+ if !bytes.Equal(stemNode.getValue(10), values[10]) {
t.Errorf("Value at index 10 mismatch")
}
- if !bytes.Equal(stemNode.Values[255], values[255]) {
+ if !bytes.Equal(stemNode.getValue(255), values[255]) {
t.Errorf("Value at index 255 mismatch")
}
@@ -140,43 +170,44 @@ func TestSerializeDeserializeStemNode(t *testing.T) {
if i == 0 || i == 10 || i == 255 {
continue
}
- if stemNode.Values[i] != nil {
- t.Errorf("Expected nil value at index %d, got %x", i, stemNode.Values[i])
+ if stemNode.hasValue(byte(i)) {
+ t.Errorf("Expected no value at index %d, got %x", i, stemNode.getValue(byte(i)))
}
}
}
-// TestDeserializeEmptyNode tests deserialization of empty node
+// TestDeserializeEmptyNode tests deserialization of empty node.
func TestDeserializeEmptyNode(t *testing.T) {
- // Empty byte slice should deserialize to Empty node
- deserialized, err := DeserializeNode([]byte{}, 0)
+ s := newNodeStore()
+ deserialized, err := s.deserializeNode([]byte{}, 0)
if err != nil {
t.Fatalf("Failed to deserialize empty node: %v", err)
}
- _, ok := deserialized.(Empty)
- if !ok {
- t.Fatalf("Expected Empty node, got %T", deserialized)
+ if !deserialized.IsEmpty() {
+ t.Fatalf("Expected emptyRef, got kind %d", deserialized.Kind())
}
}
-// TestDeserializeInvalidType tests deserialization with invalid type byte
+// TestDeserializeInvalidType tests deserialization with invalid type byte.
func TestDeserializeInvalidType(t *testing.T) {
- // Create invalid serialized data with unknown type byte
+ s := newNodeStore()
invalidData := []byte{99, 0, 0, 0} // Type byte 99 is invalid
- _, err := DeserializeNode(invalidData, 0)
+ _, err := s.deserializeNode(invalidData, 0)
if err == nil {
t.Fatal("Expected error for invalid type byte, got nil")
}
}
-// TestDeserializeInvalidLength tests deserialization with invalid data length
+// TestDeserializeInvalidLength tests deserialization with invalid data length.
func TestDeserializeInvalidLength(t *testing.T) {
- // InternalNode with type byte 1 but wrong length
- invalidData := []byte{nodeTypeInternal, 0, 0} // Too short for internal node
+ s := newNodeStore()
+ // 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 := DeserializeNode(invalidData, 0)
+ _, err := s.deserializeNode(invalidData, 0)
if err == nil {
t.Fatal("Expected error for invalid data length, got nil")
}
@@ -186,7 +217,22 @@ func TestDeserializeInvalidLength(t *testing.T) {
}
}
-// TestKeyToPath tests the keyToPath function
+// 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 {
name string
@@ -218,14 +264,14 @@ func TestKeyToPath(t *testing.T) {
},
{
name: "max valid depth",
- depth: StemSize * 8,
+ depth: StemSize*8 - 1,
key: make([]byte, HashSize),
- expected: make([]byte, StemSize*8+1),
+ expected: make([]byte, StemSize*8),
wantErr: false,
},
{
name: "depth too large",
- depth: StemSize*8 + 1,
+ depth: StemSize * 8,
key: make([]byte, HashSize),
wantErr: true,
},
diff --git a/trie/bintrie/empty.go b/trie/bintrie/empty.go
deleted file mode 100644
index 252146a4a7..0000000000
--- a/trie/bintrie/empty.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2025 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 bintrie
-
-import (
- "slices"
-
- "github.com/ethereum/go-ethereum/common"
-)
-
-type Empty struct{}
-
-func (e Empty) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
- return nil, nil
-}
-
-func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
- var values [256][]byte
- values[key[31]] = value
- return &StemNode{
- Stem: slices.Clone(key[:31]),
- Values: values[:],
- depth: depth,
- mustRecompute: true,
- }, nil
-}
-
-func (e Empty) Copy() BinaryNode {
- return Empty{}
-}
-
-func (e Empty) Hash() common.Hash {
- return common.Hash{}
-}
-
-func (e Empty) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
- var values [256][]byte
- return values[:], nil
-}
-
-func (e Empty) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
- return &StemNode{
- Stem: slices.Clone(key[:31]),
- Values: values,
- depth: depth,
- mustRecompute: true,
- }, nil
-}
-
-func (e Empty) CollectNodes(_ []byte, _ NodeFlushFn) error {
- return nil
-}
-
-func (e Empty) toDot(parent string, path string) string {
- return ""
-}
-
-func (e Empty) GetHeight() int {
- return 0
-}
diff --git a/trie/bintrie/empty_test.go b/trie/bintrie/empty_test.go
deleted file mode 100644
index 574ae1830b..0000000000
--- a/trie/bintrie/empty_test.go
+++ /dev/null
@@ -1,222 +0,0 @@
-// Copyright 2025 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 bintrie
-
-import (
- "bytes"
- "testing"
-
- "github.com/ethereum/go-ethereum/common"
-)
-
-// TestEmptyGet tests the Get method
-func TestEmptyGet(t *testing.T) {
- node := Empty{}
-
- key := make([]byte, 32)
- value, err := node.Get(key, nil)
- if err != nil {
- t.Fatalf("Unexpected error: %v", err)
- }
-
- if value != nil {
- t.Errorf("Expected nil value from empty node, got %x", value)
- }
-}
-
-// TestEmptyInsert tests the Insert method
-func TestEmptyInsert(t *testing.T) {
- node := Empty{}
-
- key := make([]byte, 32)
- key[0] = 0x12
- key[31] = 0x34
- value := common.HexToHash("0xabcd").Bytes()
-
- newNode, err := node.Insert(key, value, nil, 0)
- if err != nil {
- t.Fatalf("Failed to insert: %v", err)
- }
-
- // Should create a StemNode
- stemNode, ok := newNode.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", newNode)
- }
-
- // Check the stem (first 31 bytes of key)
- if !bytes.Equal(stemNode.Stem, key[:31]) {
- t.Errorf("Stem mismatch: expected %x, got %x", key[:31], stemNode.Stem)
- }
-
- // Check the value at the correct index (last byte of key)
- if !bytes.Equal(stemNode.Values[key[31]], value) {
- t.Errorf("Value mismatch at index %d: expected %x, got %x", key[31], value, stemNode.Values[key[31]])
- }
-
- // Check that other values are nil
- for i := 0; i < 256; i++ {
- if i != int(key[31]) && stemNode.Values[i] != nil {
- t.Errorf("Expected nil value at index %d, got %x", i, stemNode.Values[i])
- }
- }
-}
-
-// TestEmptyCopy tests the Copy method
-func TestEmptyCopy(t *testing.T) {
- node := Empty{}
-
- copied := node.Copy()
- copiedEmpty, ok := copied.(Empty)
- if !ok {
- t.Fatalf("Expected Empty, got %T", copied)
- }
-
- // Both should be empty
- if node != copiedEmpty {
- // Empty is a zero-value struct, so copies should be equal
- t.Errorf("Empty nodes should be equal")
- }
-}
-
-// TestEmptyHash tests the Hash method
-func TestEmptyHash(t *testing.T) {
- node := Empty{}
-
- hash := node.Hash()
-
- // Empty node should have zero hash
- if hash != (common.Hash{}) {
- t.Errorf("Expected zero hash for empty node, got %x", hash)
- }
-}
-
-// TestEmptyGetValuesAtStem tests the GetValuesAtStem method
-func TestEmptyGetValuesAtStem(t *testing.T) {
- node := Empty{}
-
- stem := make([]byte, 31)
- values, err := node.GetValuesAtStem(stem, nil)
- if err != nil {
- t.Fatalf("Unexpected error: %v", err)
- }
-
- // Should return an array of 256 nil values
- if len(values) != 256 {
- t.Errorf("Expected 256 values, got %d", len(values))
- }
-
- for i, v := range values {
- if v != nil {
- t.Errorf("Expected nil value at index %d, got %x", i, v)
- }
- }
-}
-
-// TestEmptyInsertValuesAtStem tests the InsertValuesAtStem method
-func TestEmptyInsertValuesAtStem(t *testing.T) {
- node := Empty{}
-
- stem := make([]byte, 31)
- stem[0] = 0x42
-
- var values [256][]byte
- values[0] = common.HexToHash("0x0101").Bytes()
- values[10] = common.HexToHash("0x0202").Bytes()
- values[255] = common.HexToHash("0x0303").Bytes()
-
- newNode, err := node.InsertValuesAtStem(stem, values[:], nil, 5)
- if err != nil {
- t.Fatalf("Failed to insert values: %v", err)
- }
-
- // Should create a StemNode
- stemNode, ok := newNode.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", newNode)
- }
-
- // Check the stem
- if !bytes.Equal(stemNode.Stem, stem) {
- t.Errorf("Stem mismatch: expected %x, got %x", stem, stemNode.Stem)
- }
-
- // Check the depth
- if stemNode.depth != 5 {
- t.Errorf("Depth mismatch: expected 5, got %d", stemNode.depth)
- }
-
- // Check the values
- if !bytes.Equal(stemNode.Values[0], values[0]) {
- t.Error("Value at index 0 mismatch")
- }
- if !bytes.Equal(stemNode.Values[10], values[10]) {
- t.Error("Value at index 10 mismatch")
- }
- if !bytes.Equal(stemNode.Values[255], values[255]) {
- t.Error("Value at index 255 mismatch")
- }
-
- // Check that values is the same slice (not a copy)
- if &stemNode.Values[0] != &values[0] {
- t.Error("Expected values to be the same slice reference")
- }
-}
-
-// TestEmptyCollectNodes tests the CollectNodes method
-func TestEmptyCollectNodes(t *testing.T) {
- node := Empty{}
-
- var collected []BinaryNode
- flushFn := func(path []byte, n BinaryNode) {
- collected = append(collected, n)
- }
-
- err := node.CollectNodes([]byte{0, 1, 0}, flushFn)
- if err != nil {
- t.Fatalf("Unexpected error: %v", err)
- }
-
- // Should not collect anything for empty node
- if len(collected) != 0 {
- t.Errorf("Expected no collected nodes for empty, got %d", len(collected))
- }
-}
-
-// TestEmptyToDot tests the toDot method
-func TestEmptyToDot(t *testing.T) {
- node := Empty{}
-
- dot := node.toDot("parent", "010")
-
- // Should return empty string for empty node
- if dot != "" {
- t.Errorf("Expected empty string for empty node toDot, got %s", dot)
- }
-}
-
-// TestEmptyGetHeight tests the GetHeight method
-func TestEmptyGetHeight(t *testing.T) {
- node := Empty{}
-
- height := node.GetHeight()
-
- // Empty node should have height 0
- if height != 0 {
- t.Errorf("Expected height 0 for empty node, got %d", height)
- }
-}
diff --git a/trie/bintrie/hashed_node.go b/trie/bintrie/hashed_node.go
index e44c6d1e8a..b176df079b 100644
--- a/trie/bintrie/hashed_node.go
+++ b/trie/bintrie/hashed_node.go
@@ -16,75 +16,10 @@
package bintrie
-import (
- "errors"
- "fmt"
-
- "github.com/ethereum/go-ethereum/common"
-)
+import "github.com/ethereum/go-ethereum/common"
+// HashedNode is an unresolved node — only its hash is known.
type HashedNode common.Hash
-func (h HashedNode) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
- panic("not implemented") // TODO: Implement
-}
-
-func (h HashedNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
- return nil, errors.New("insert not implemented for hashed node")
-}
-
-func (h HashedNode) Copy() BinaryNode {
- nh := common.Hash(h)
- return HashedNode(nh)
-}
-
-func (h HashedNode) Hash() common.Hash {
- return common.Hash(h)
-}
-
-func (h HashedNode) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
- return nil, errors.New("attempted to get values from an unresolved node")
-}
-
-func (h HashedNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
- // Step 1: Generate the path for this node's position in the tree
- path, err := keyToPath(depth, stem)
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem path generation error: %w", err)
- }
-
- if resolver == nil {
- return nil, errors.New("InsertValuesAtStem resolve error: resolver is nil")
- }
-
- // Step 2: Resolve the hashed node to get the actual node data
- data, err := resolver(path, common.Hash(h))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
- }
-
- // Step 3: Deserialize the resolved data into a concrete node
- node, err := DeserializeNodeWithHash(data, depth, common.Hash(h))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
- }
-
- // Step 4: Call InsertValuesAtStem on the resolved concrete node
- return node.InsertValuesAtStem(stem, values, resolver, depth)
-}
-
-func (h HashedNode) toDot(parent string, path string) string {
- me := fmt.Sprintf("hash%s", path)
- ret := fmt.Sprintf("%s [label=\"%x\"]\n", me, h)
- ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
- return ret
-}
-
-func (h HashedNode) CollectNodes([]byte, NodeFlushFn) error {
- // HashedNodes are already persisted in the database and don't need to be collected.
- return nil
-}
-
-func (h HashedNode) GetHeight() int {
- panic("tried to get the height of a hashed node, this is a bug")
-}
+// Hash returns the node's hash.
+func (h HashedNode) Hash() common.Hash { return common.Hash(h) }
diff --git a/trie/bintrie/hashed_node_test.go b/trie/bintrie/hashed_node_test.go
index f9e6984888..2e12bfba5e 100644
--- a/trie/bintrie/hashed_node_test.go
+++ b/trie/bintrie/hashed_node_test.go
@@ -18,180 +18,137 @@ package bintrie
import (
"bytes"
+ "errors"
"testing"
"github.com/ethereum/go-ethereum/common"
)
-// TestHashedNodeHash tests the Hash method
+// TestHashedNodeHash tests the Hash method via nodeStore.
func TestHashedNodeHash(t *testing.T) {
hash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
- node := HashedNode(hash)
+ s := newNodeStore()
+ ref := s.newHashedRef(hash)
- // Hash should return the stored hash
- if node.Hash() != hash {
- t.Errorf("Hash mismatch: expected %x, got %x", hash, node.Hash())
+ if s.computeHash(ref) != hash {
+ t.Errorf("Hash mismatch: expected %x, got %x", hash, s.computeHash(ref))
}
}
-// TestHashedNodeCopy tests the Copy method
+// TestHashedNodeCopy tests the Copy method via nodeStore.
func TestHashedNodeCopy(t *testing.T) {
hash := common.HexToHash("0xabcdef")
- node := HashedNode(hash)
+ s := newNodeStore()
+ ref := s.newHashedRef(hash)
+ s.root = ref
- copied := node.Copy()
- copiedHash, ok := copied.(HashedNode)
- if !ok {
- t.Fatalf("Expected HashedNode, got %T", copied)
- }
+ ns := s.Copy()
+ copiedHash := ns.computeHash(ns.root)
- // Hash should be the same
- if common.Hash(copiedHash) != hash {
+ if copiedHash != hash {
t.Errorf("Hash mismatch after copy: expected %x, got %x", hash, copiedHash)
}
-
- // But should be a different object
- if &node == &copiedHash {
- t.Error("Copy returned same object reference")
- }
}
-// TestHashedNodeInsert tests that Insert returns an error
-func TestHashedNodeInsert(t *testing.T) {
- node := HashedNode(common.HexToHash("0x1234"))
-
- key := make([]byte, HashSize)
- value := make([]byte, HashSize)
-
- _, err := node.Insert(key, value, nil, 0)
- if err == nil {
- t.Fatal("Expected error for Insert on HashedNode")
- }
-
- if err.Error() != "insert not implemented for hashed node" {
- t.Errorf("Unexpected error message: %v", err)
- }
-}
-
-// TestHashedNodeGetValuesAtStem tests that GetValuesAtStem returns an error
-func TestHashedNodeGetValuesAtStem(t *testing.T) {
- node := HashedNode(common.HexToHash("0x1234"))
-
- stem := make([]byte, StemSize)
- _, err := node.GetValuesAtStem(stem, nil)
- if err == nil {
- t.Fatal("Expected error for GetValuesAtStem on HashedNode")
- }
-
- if err.Error() != "attempted to get values from an unresolved node" {
- t.Errorf("Unexpected error message: %v", err)
- }
-}
-
-// TestHashedNodeInsertValuesAtStem tests that InsertValuesAtStem returns an error
+// TestHashedNodeInsertValuesAtStem tests InsertValuesAtStem resolution via nodeStore.
func TestHashedNodeInsertValuesAtStem(t *testing.T) {
- node := HashedNode(common.HexToHash("0x1234"))
+ // Test 1: nil resolver should return an error
+ s := newNodeStore()
+ hashedRef := s.newHashedRef(common.HexToHash("0x1234"))
+ s.root = hashedRef
stem := make([]byte, StemSize)
values := make([][]byte, StemNodeWidth)
- // Test 1: nil resolver should return an error
- _, err := node.InsertValuesAtStem(stem, values, nil, 0)
+ err := s.InsertValuesAtStem(stem, values, nil)
if err == nil {
- t.Fatal("Expected error for InsertValuesAtStem on HashedNode with nil resolver")
- }
-
- if err.Error() != "InsertValuesAtStem resolve error: resolver is nil" {
- t.Errorf("Unexpected error message: %v", err)
+ t.Fatal("Expected error for InsertValuesAtStem with nil resolver")
}
// Test 2: mock resolver returning invalid data should return deserialization error
mockResolver := func(path []byte, hash common.Hash) ([]byte, error) {
- // Return invalid/nonsense data that cannot be deserialized
return []byte{0xff, 0xff, 0xff}, nil
}
- _, err = node.InsertValuesAtStem(stem, values, mockResolver, 0)
- if err == nil {
- t.Fatal("Expected error for InsertValuesAtStem on HashedNode with invalid resolver data")
- }
+ s2 := newNodeStore()
+ hashedRef2 := s2.newHashedRef(common.HexToHash("0x1234"))
+ s2.root = hashedRef2
- expectedPrefix := "InsertValuesAtStem node deserialization error:"
- if len(err.Error()) < len(expectedPrefix) || err.Error()[:len(expectedPrefix)] != expectedPrefix {
- t.Errorf("Expected deserialization error, got: %v", err)
+ err = s2.InsertValuesAtStem(stem, values, mockResolver)
+ if err == nil {
+ t.Fatal("Expected error for InsertValuesAtStem with invalid resolver data")
}
// Test 3: mock resolver returning valid serialized node should succeed
stem = make([]byte, StemSize)
stem[0] = 0xaa
- var originalValues [StemNodeWidth][]byte
+ originalValues := make([][]byte, StemNodeWidth)
originalValues[0] = common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111").Bytes()
originalValues[1] = common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222").Bytes()
- originalNode := &StemNode{
- Stem: stem,
- Values: originalValues[:],
- depth: 0,
+ // Build the serialized node
+ rs := newNodeStore()
+ ref := rs.newStemRef(stem, 0)
+ sn := rs.getStem(ref.Index())
+ for i, v := range originalValues {
+ if v != nil {
+ sn.setValue(byte(i), v)
+ }
}
+ serialized := rs.serializeNode(ref, 8)
- // Serialize the node
- serialized := SerializeNode(originalNode)
-
- // Create a mock resolver that returns the serialized node
validResolver := func(path []byte, hash common.Hash) ([]byte, error) {
return serialized, nil
}
- var newValues [StemNodeWidth][]byte
+ s3 := newNodeStore()
+ hashedRef3 := s3.newHashedRef(common.HexToHash("0x1234"))
+ s3.root = hashedRef3
+
+ newValues := make([][]byte, StemNodeWidth)
newValues[2] = common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333").Bytes()
- resolvedNode, err := node.InsertValuesAtStem(stem, newValues[:], validResolver, 0)
+ err = s3.InsertValuesAtStem(stem, newValues, validResolver)
if err != nil {
t.Fatalf("Expected successful resolution and insertion, got error: %v", err)
}
- resultStem, ok := resolvedNode.(*StemNode)
- if !ok {
- t.Fatalf("Expected resolved node to be *StemNode, got %T", resolvedNode)
+ // Verify original values are preserved
+ retrieved, err := s3.GetValuesAtStem(stem, nil)
+ if err != nil {
+ t.Fatal(err)
}
-
- if !bytes.Equal(resultStem.Stem, stem) {
- t.Errorf("Stem mismatch: expected %x, got %x", stem, resultStem.Stem)
+ if !bytes.Equal(retrieved[0], originalValues[0]) {
+ t.Errorf("Original value at index 0 not preserved")
}
-
- // Verify the original values are preserved
- if !bytes.Equal(resultStem.Values[0], originalValues[0]) {
- t.Errorf("Original value at index 0 not preserved: expected %x, got %x", originalValues[0], resultStem.Values[0])
+ if !bytes.Equal(retrieved[1], originalValues[1]) {
+ t.Errorf("Original value at index 1 not preserved")
}
- if !bytes.Equal(resultStem.Values[1], originalValues[1]) {
- t.Errorf("Original value at index 1 not preserved: expected %x, got %x", originalValues[1], resultStem.Values[1])
- }
-
- // Verify the new value was inserted
- if !bytes.Equal(resultStem.Values[2], newValues[2]) {
- t.Errorf("New value at index 2 not inserted correctly: expected %x, got %x", newValues[2], resultStem.Values[2])
+ if !bytes.Equal(retrieved[2], newValues[2]) {
+ t.Errorf("New value at index 2 not inserted correctly")
}
}
-// TestHashedNodeToDot tests the toDot method for visualization
-func TestHashedNodeToDot(t *testing.T) {
- hash := common.HexToHash("0x1234")
- node := HashedNode(hash)
+// TestHashedNodeGetError tests that getting through an unresolved HashedNode root returns error.
+func TestHashedNodeGetError(t *testing.T) {
+ s := newNodeStore()
+ // Create root as hashed, then try to resolve through InternalNode parent
+ rootRef := s.newInternalRef(0)
+ rootNode := s.getInternal(rootRef.Index())
+ hashedLeft := s.newHashedRef(common.HexToHash("0x1234"))
+ rootNode.left = hashedLeft
+ rootNode.right = emptyRef
+ s.root = rootRef
- dot := node.toDot("parent", "010")
+ key := make([]byte, 32) // goes left
+ key[31] = 5
- // Should contain the hash value and parent connection
- expectedHash := "hash010"
- if !contains(dot, expectedHash) {
- t.Errorf("Expected dot output to contain %s", expectedHash)
+ resolver := func(path []byte, hash common.Hash) ([]byte, error) {
+ return nil, errors.New("node not found")
}
- if !contains(dot, "parent -> hash010") {
- t.Error("Expected dot output to contain parent connection")
+ _, err := s.Get(key, resolver)
+ if err == nil {
+ t.Fatal("Expected error when resolver fails")
}
}
-
-// Helper function
-func contains(s, substr string) bool {
- return len(s) >= len(substr) && s != "" && len(substr) > 0
-}
diff --git a/trie/bintrie/internal_node.go b/trie/bintrie/internal_node.go
index 946203bcfb..b83cb92d87 100644
--- a/trie/bintrie/internal_node.go
+++ b/trie/bintrie/internal_node.go
@@ -17,35 +17,13 @@
package bintrie
import (
- "crypto/sha256"
"errors"
- "fmt"
- "math/bits"
- "runtime"
- "sync"
"github.com/ethereum/go-ethereum/common"
)
-// parallelDepth returns the tree depth below which Hash() spawns goroutines.
-func parallelDepth() int {
- return min(bits.Len(uint(runtime.NumCPU())), 8)
-}
-
-// isDirty reports whether a BinaryNode child needs rehashing.
-func isDirty(n BinaryNode) bool {
- switch v := n.(type) {
- case *InternalNode:
- return v.mustRecompute
- case *StemNode:
- return v.mustRecompute
- default:
- return false
- }
-}
-
func keyToPath(depth int, key []byte) ([]byte, error) {
- if depth > 31*8 {
+ if depth >= 31*8 {
return nil, errors.New("node too deep")
}
path := make([]byte, 0, depth+1)
@@ -56,243 +34,12 @@ func keyToPath(depth int, key []byte) ([]byte, error) {
return path, nil
}
-// InternalNode is a binary trie internal node.
+// Invariant: dirty=false implies mustRecompute=false. Every mutation that
+// invalidates the cached hash MUST also mark the blob for re-flush.
type InternalNode struct {
- left, right BinaryNode
- depth int
-
- mustRecompute bool // true if the hash needs to be recomputed
- hash common.Hash // cached hash when mustRecompute == false
-}
-
-// GetValuesAtStem retrieves the group of values located at the given stem key.
-func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([][]byte, error) {
- if bt.depth > 31*8 {
- return nil, errors.New("node too deep")
- }
-
- bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
- if bit == 0 {
- if hn, ok := bt.left.(HashedNode); ok {
- path, err := keyToPath(bt.depth, stem)
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
- }
- data, err := resolver(path, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
- }
- node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
- }
- bt.left = node
- }
- return bt.left.GetValuesAtStem(stem, resolver)
- }
-
- if hn, ok := bt.right.(HashedNode); ok {
- path, err := keyToPath(bt.depth, stem)
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
- }
- data, err := resolver(path, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
- }
- node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
- }
- bt.right = node
- }
- return bt.right.GetValuesAtStem(stem, resolver)
-}
-
-// Get retrieves the value for the given key.
-func (bt *InternalNode) Get(key []byte, resolver NodeResolverFn) ([]byte, error) {
- values, err := bt.GetValuesAtStem(key[:31], resolver)
- if err != nil {
- return nil, fmt.Errorf("get error: %w", err)
- }
- if values == nil {
- return nil, nil
- }
- return values[key[31]], nil
-}
-
-// Insert inserts a new key-value pair into the trie.
-func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
- var values [256][]byte
- values[key[31]] = value
- return bt.InsertValuesAtStem(key[:31], values[:], resolver, depth)
-}
-
-// Copy creates a deep copy of the node.
-func (bt *InternalNode) Copy() BinaryNode {
- return &InternalNode{
- left: bt.left.Copy(),
- right: bt.right.Copy(),
- depth: bt.depth,
- mustRecompute: bt.mustRecompute,
- hash: bt.hash,
- }
-}
-
-// Hash returns the hash of the node.
-func (bt *InternalNode) Hash() common.Hash {
- if !bt.mustRecompute {
- return bt.hash
- }
-
- // At shallow depths, parallelize when both children need rehashing:
- // hash left subtree in a goroutine, right subtree inline, then combine.
- // Skip goroutine overhead when only one child is dirty (common case
- // for narrow state updates that touch a single path through the trie).
- if bt.depth < parallelDepth() && isDirty(bt.left) && isDirty(bt.right) {
- var input [64]byte
- var lh common.Hash
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- lh = bt.left.Hash()
- }()
- rh := bt.right.Hash()
- copy(input[32:], rh[:])
- wg.Wait()
- copy(input[:32], lh[:])
- bt.hash = sha256.Sum256(input[:])
- bt.mustRecompute = false
- return bt.hash
- }
-
- // Deeper nodes: sequential using pooled hasher (goroutine overhead > hash cost)
- h := newSha256()
- defer returnSha256(h)
- if bt.left != nil {
- h.Write(bt.left.Hash().Bytes())
- } else {
- h.Write(zero[:])
- }
- if bt.right != nil {
- h.Write(bt.right.Hash().Bytes())
- } else {
- h.Write(zero[:])
- }
- bt.hash = common.BytesToHash(h.Sum(nil))
- bt.mustRecompute = false
- return bt.hash
-}
-
-// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
-// Already-existing values will be overwritten.
-func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
- var err error
- bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
- if bit == 0 {
- if bt.left == nil {
- bt.left = Empty{}
- }
-
- if hn, ok := bt.left.(HashedNode); ok {
- path, err := keyToPath(bt.depth, stem)
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
- }
- data, err := resolver(path, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
- }
- node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
- }
- bt.left = node
- }
-
- bt.left, err = bt.left.InsertValuesAtStem(stem, values, resolver, depth+1)
- bt.mustRecompute = true
- return bt, err
- }
-
- if bt.right == nil {
- bt.right = Empty{}
- }
-
- if hn, ok := bt.right.(HashedNode); ok {
- path, err := keyToPath(bt.depth, stem)
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
- }
- data, err := resolver(path, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
- }
- node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn))
- if err != nil {
- return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
- }
- bt.right = node
- }
-
- bt.right, err = bt.right.InsertValuesAtStem(stem, values, resolver, depth+1)
- bt.mustRecompute = true
- return bt, err
-}
-
-// CollectNodes collects all child nodes at a given path, and flushes it
-// into the provided node collector.
-func (bt *InternalNode) CollectNodes(path []byte, flushfn NodeFlushFn) error {
- if bt.left != nil {
- var p [256]byte
- copy(p[:], path)
- childpath := p[:len(path)]
- childpath = append(childpath, 0)
- if err := bt.left.CollectNodes(childpath, flushfn); err != nil {
- return err
- }
- }
- if bt.right != nil {
- var p [256]byte
- copy(p[:], path)
- childpath := p[:len(path)]
- childpath = append(childpath, 1)
- if err := bt.right.CollectNodes(childpath, flushfn); err != nil {
- return err
- }
- }
- flushfn(path, bt)
- return nil
-}
-
-// GetHeight returns the height of the node.
-func (bt *InternalNode) GetHeight() int {
- var (
- leftHeight int
- rightHeight int
- )
- if bt.left != nil {
- leftHeight = bt.left.GetHeight()
- }
- if bt.right != nil {
- rightHeight = bt.right.GetHeight()
- }
- return 1 + max(leftHeight, rightHeight)
-}
-
-func (bt *InternalNode) toDot(parent, path string) string {
- me := fmt.Sprintf("internal%s", path)
- ret := fmt.Sprintf("%s [label=\"I: %x\"]\n", me, bt.Hash())
- if len(parent) > 0 {
- ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
- }
-
- if bt.left != nil {
- ret = fmt.Sprintf("%s%s", ret, bt.left.toDot(me, fmt.Sprintf("%s%02x", path, 0)))
- }
- if bt.right != nil {
- ret = fmt.Sprintf("%s%s", ret, bt.right.toDot(me, fmt.Sprintf("%s%02x", path, 1)))
- }
- return ret
+ left, right nodeRef
+ depth uint8
+ mustRecompute bool // hash is stale (cleared by Hash)
+ dirty bool // on-disk blob is stale (cleared by CollectNodes)
+ hash common.Hash
}
diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go
index 69097483fd..4d8da8af37 100644
--- a/trie/bintrie/internal_node_test.go
+++ b/trie/bintrie/internal_node_test.go
@@ -24,35 +24,33 @@ import (
"github.com/ethereum/go-ethereum/common"
)
-// TestInternalNodeGet tests the Get method
+// TestInternalNodeGet tests the Get method via nodeStore.
func TestInternalNodeGet(t *testing.T) {
- // Create a simple tree structure
+ s := newNodeStore()
+
leftStem := make([]byte, 31)
rightStem := make([]byte, 31)
- rightStem[0] = 0x80 // First bit is 1
+ rightStem[0] = 0x80
- var leftValues, rightValues [256][]byte
+ leftValues := make([][]byte, 256)
leftValues[0] = common.HexToHash("0x0101").Bytes()
+ rightValues := make([][]byte, 256)
rightValues[0] = common.HexToHash("0x0202").Bytes()
- node := &InternalNode{
- depth: 0,
- left: &StemNode{
- Stem: leftStem,
- Values: leftValues[:],
- depth: 1,
- },
- right: &StemNode{
- Stem: rightStem,
- Values: rightValues[:],
- depth: 1,
- },
+ // Build tree: root -> left stem, right stem
+ // Insert left stem values
+ s.root = emptyRef
+ if err := s.InsertValuesAtStem(leftStem, leftValues, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.InsertValuesAtStem(rightStem, rightValues, nil); err != nil {
+ t.Fatal(err)
}
// Get value from left subtree
leftKey := make([]byte, 32)
leftKey[31] = 0
- value, err := node.Get(leftKey, nil)
+ value, err := s.Get(leftKey, nil)
if err != nil {
t.Fatalf("Failed to get left value: %v", err)
}
@@ -64,7 +62,7 @@ func TestInternalNodeGet(t *testing.T) {
rightKey := make([]byte, 32)
rightKey[0] = 0x80
rightKey[31] = 0
- value, err = node.Get(rightKey, nil)
+ value, err = s.Get(rightKey, nil)
if err != nil {
t.Fatalf("Failed to get right value: %v", err)
}
@@ -73,29 +71,26 @@ func TestInternalNodeGet(t *testing.T) {
}
}
-// TestInternalNodeGetWithResolver tests Get with HashedNode resolution
+// TestInternalNodeGetWithResolver tests Get with HashedNode resolution via nodeStore.
func TestInternalNodeGetWithResolver(t *testing.T) {
- // Create an internal node with a hashed child
- hashedChild := HashedNode(common.HexToHash("0x1234"))
-
- node := &InternalNode{
- depth: 0,
- left: hashedChild,
- right: Empty{},
- }
+ // Create a store with an internal node containing a hashed child
+ s := newNodeStore()
+ hashedChild := s.newHashedRef(common.HexToHash("0x1234"))
+ rootRef := s.newInternalRef(0)
+ rootNode := s.getInternal(rootRef.Index())
+ rootNode.left = hashedChild
+ rootNode.right = emptyRef
+ s.root = rootRef
// Mock resolver that returns a stem node
resolver := func(path []byte, hash common.Hash) ([]byte, error) {
- if hash == common.Hash(hashedChild) {
+ if hash == common.HexToHash("0x1234") {
+ rs := newNodeStore()
stem := make([]byte, 31)
- var values [256][]byte
- values[5] = common.HexToHash("0xabcd").Bytes()
- stemNode := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 1,
- }
- return SerializeNode(stemNode), nil
+ ref := rs.newStemRef(stem, 1)
+ sn := rs.getStem(ref.Index())
+ sn.setValue(5, common.HexToHash("0xabcd").Bytes())
+ return rs.serializeNode(ref, 8), nil
}
return nil, errors.New("node not found")
}
@@ -103,7 +98,7 @@ func TestInternalNodeGetWithResolver(t *testing.T) {
// Get value through the hashed node
key := make([]byte, 32)
key[31] = 5
- value, err := node.Get(key, resolver)
+ value, err := s.Get(key, resolver)
if err != nil {
t.Fatalf("Failed to get value: %v", err)
}
@@ -114,179 +109,113 @@ func TestInternalNodeGetWithResolver(t *testing.T) {
}
}
-// TestInternalNodeInsert tests the Insert method
+// TestInternalNodeInsert tests the Insert method via nodeStore.
func TestInternalNodeInsert(t *testing.T) {
- // Start with an internal node with empty children
- node := &InternalNode{
- depth: 0,
- left: Empty{},
- right: Empty{},
- }
+ s := newNodeStore()
- // Insert a value into the left subtree
leftKey := make([]byte, 32)
leftKey[31] = 10
leftValue := common.HexToHash("0x0101").Bytes()
- newNode, err := node.Insert(leftKey, leftValue, nil, 0)
- if err != nil {
+ if err := s.Insert(leftKey, leftValue, nil); err != nil {
t.Fatalf("Failed to insert: %v", err)
}
- internalNode, ok := newNode.(*InternalNode)
- if !ok {
- t.Fatalf("Expected InternalNode, got %T", newNode)
+ // Verify the value was stored
+ value, err := s.Get(leftKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to get: %v", err)
}
-
- // Check that left child is now a StemNode
- leftStem, ok := internalNode.left.(*StemNode)
- if !ok {
- t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
- }
-
- // Check the inserted value
- if !bytes.Equal(leftStem.Values[10], leftValue) {
- t.Errorf("Value mismatch: expected %x, got %x", leftValue, leftStem.Values[10])
- }
-
- // Right child should still be Empty
- _, ok = internalNode.right.(Empty)
- if !ok {
- t.Errorf("Expected right child to remain Empty, got %T", internalNode.right)
+ if !bytes.Equal(value, leftValue) {
+ t.Errorf("Value mismatch: expected %x, got %x", leftValue, value)
}
}
-// TestInternalNodeCopy tests the Copy method
+// TestInternalNodeCopy tests the Copy method via nodeStore.
func TestInternalNodeCopy(t *testing.T) {
- // Create an internal node with stem children
- leftStem := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 1,
- }
- leftStem.Values[0] = common.HexToHash("0x0101").Bytes()
+ s := newNodeStore()
- rightStem := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 1,
- }
- rightStem.Stem[0] = 0x80
- rightStem.Values[0] = common.HexToHash("0x0202").Bytes()
+ leftKey := make([]byte, 32)
+ leftKey[31] = 0
+ leftValue := common.HexToHash("0x0101").Bytes()
- node := &InternalNode{
- depth: 0,
- left: leftStem,
- right: rightStem,
+ rightKey := make([]byte, 32)
+ rightKey[0] = 0x80
+ rightKey[31] = 0
+ rightValue := common.HexToHash("0x0202").Bytes()
+
+ if err := s.Insert(leftKey, leftValue, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Insert(rightKey, rightValue, nil); err != nil {
+ t.Fatal(err)
}
- // Create a copy
- copied := node.Copy()
- copiedInternal, ok := copied.(*InternalNode)
- if !ok {
- t.Fatalf("Expected InternalNode, got %T", copied)
- }
+ ns := s.Copy()
- // Check depth
- if copiedInternal.depth != node.depth {
- t.Errorf("Depth mismatch: expected %d, got %d", node.depth, copiedInternal.depth)
- }
-
- // Check that children are copied
- copiedLeft, ok := copiedInternal.left.(*StemNode)
- if !ok {
- t.Fatalf("Expected left child to be StemNode, got %T", copiedInternal.left)
- }
-
- copiedRight, ok := copiedInternal.right.(*StemNode)
- if !ok {
- t.Fatalf("Expected right child to be StemNode, got %T", copiedInternal.right)
- }
-
- // Verify deep copy (children should be different objects)
- if copiedLeft == leftStem {
- t.Error("Left child not properly copied")
- }
- if copiedRight == rightStem {
- t.Error("Right child not properly copied")
- }
-
- // But values should be equal
- if !bytes.Equal(copiedLeft.Values[0], leftStem.Values[0]) {
+ // Values should be equal
+ v1, _ := ns.Get(leftKey, nil)
+ if !bytes.Equal(v1, leftValue) {
t.Error("Left child value mismatch after copy")
}
- if !bytes.Equal(copiedRight.Values[0], rightStem.Values[0]) {
+ v2, _ := ns.Get(rightKey, nil)
+ if !bytes.Equal(v2, rightValue) {
t.Error("Right child value mismatch after copy")
}
}
-// TestInternalNodeHash tests the Hash method
+// TestInternalNodeHash tests the Hash method via nodeStore.
func TestInternalNodeHash(t *testing.T) {
- // Create an internal node
- node := &InternalNode{
- depth: 0,
- left: HashedNode(common.HexToHash("0x1111")),
- right: HashedNode(common.HexToHash("0x2222")),
- }
+ s := newNodeStore()
+ leftRef := s.newHashedRef(common.HexToHash("0x1111"))
+ rightRef := s.newHashedRef(common.HexToHash("0x2222"))
+ rootRef := s.newInternalRef(0)
+ rootNode := s.getInternal(rootRef.Index())
+ rootNode.left = leftRef
+ rootNode.right = rightRef
+ s.root = rootRef
- hash1 := node.Hash()
+ hash1 := s.computeHash(rootRef)
// Hash should be deterministic
- hash2 := node.Hash()
+ hash2 := s.computeHash(rootRef)
if hash1 != hash2 {
t.Errorf("Hash not deterministic: %x != %x", hash1, hash2)
}
// Changing a child should change the hash
- node.left = HashedNode(common.HexToHash("0x3333"))
- node.mustRecompute = true
- hash3 := node.Hash()
+ rootNode.left = s.newHashedRef(common.HexToHash("0x3333"))
+ rootNode.mustRecompute = true
+ hash3 := s.computeHash(rootRef)
if hash1 == hash3 {
t.Error("Hash didn't change after modifying left child")
}
-
- // Test with nil children (should use zero hash)
- nodeWithNil := &InternalNode{
- depth: 0,
- left: nil,
- right: HashedNode(common.HexToHash("0x4444")),
- mustRecompute: true,
- }
- hashWithNil := nodeWithNil.Hash()
- if hashWithNil == (common.Hash{}) {
- t.Error("Hash shouldn't be zero even with nil child")
- }
}
-// TestInternalNodeGetValuesAtStem tests GetValuesAtStem method
+// TestInternalNodeGetValuesAtStem tests GetValuesAtStem method via nodeStore.
func TestInternalNodeGetValuesAtStem(t *testing.T) {
- // Create a tree with values at different stems
+ s := newNodeStore()
+
leftStem := make([]byte, 31)
rightStem := make([]byte, 31)
rightStem[0] = 0x80
- var leftValues, rightValues [256][]byte
+ leftValues := make([][]byte, 256)
leftValues[0] = common.HexToHash("0x0101").Bytes()
leftValues[10] = common.HexToHash("0x0102").Bytes()
+ rightValues := make([][]byte, 256)
rightValues[0] = common.HexToHash("0x0201").Bytes()
rightValues[20] = common.HexToHash("0x0202").Bytes()
- node := &InternalNode{
- depth: 0,
- left: &StemNode{
- Stem: leftStem,
- Values: leftValues[:],
- depth: 1,
- },
- right: &StemNode{
- Stem: rightStem,
- Values: rightValues[:],
- depth: 1,
- },
+ if err := s.InsertValuesAtStem(leftStem, leftValues, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.InsertValuesAtStem(rightStem, rightValues, nil); err != nil {
+ t.Fatal(err)
}
// Get values from left stem
- values, err := node.GetValuesAtStem(leftStem, nil)
+ values, err := s.GetValuesAtStem(leftStem, nil)
if err != nil {
t.Fatalf("Failed to get left values: %v", err)
}
@@ -298,7 +227,7 @@ func TestInternalNodeGetValuesAtStem(t *testing.T) {
}
// Get values from right stem
- values, err = node.GetValuesAtStem(rightStem, nil)
+ values, err = s.GetValuesAtStem(rightStem, nil)
if err != nil {
t.Fatalf("Failed to get right values: %v", err)
}
@@ -310,151 +239,100 @@ func TestInternalNodeGetValuesAtStem(t *testing.T) {
}
}
-// TestInternalNodeInsertValuesAtStem tests InsertValuesAtStem method
+// TestInternalNodeInsertValuesAtStem tests InsertValuesAtStem method via nodeStore.
func TestInternalNodeInsertValuesAtStem(t *testing.T) {
- // Start with an internal node with empty children
- node := &InternalNode{
- depth: 0,
- left: Empty{},
- right: Empty{},
- }
+ s := newNodeStore()
- // Insert values at a stem in the left subtree
stem := make([]byte, 31)
- var values [256][]byte
+ values := make([][]byte, 256)
values[5] = common.HexToHash("0x0505").Bytes()
values[10] = common.HexToHash("0x1010").Bytes()
- newNode, err := node.InsertValuesAtStem(stem, values[:], nil, 0)
- if err != nil {
+ if err := s.InsertValuesAtStem(stem, values, nil); err != nil {
t.Fatalf("Failed to insert values: %v", err)
}
- internalNode, ok := newNode.(*InternalNode)
- if !ok {
- t.Fatalf("Expected InternalNode, got %T", newNode)
+ // Check that the values are stored
+ retrieved, err := s.GetValuesAtStem(stem, nil)
+ if err != nil {
+ t.Fatalf("Failed to get values: %v", err)
}
-
- // Check that left child is now a StemNode with the values
- leftStem, ok := internalNode.left.(*StemNode)
- if !ok {
- t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
- }
-
- if !bytes.Equal(leftStem.Values[5], values[5]) {
+ if !bytes.Equal(retrieved[5], values[5]) {
t.Error("Value at index 5 mismatch")
}
- if !bytes.Equal(leftStem.Values[10], values[10]) {
+ if !bytes.Equal(retrieved[10], values[10]) {
t.Error("Value at index 10 mismatch")
}
}
-// TestInternalNodeCollectNodes tests CollectNodes method
+// TestInternalNodeCollectNodes tests CollectNodes method via nodeStore.
func TestInternalNodeCollectNodes(t *testing.T) {
- // Create an internal node with two stem children
- leftStem := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 1,
- }
+ s := newNodeStore()
- rightStem := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 1,
- }
- rightStem.Stem[0] = 0x80
+ leftStem := make([]byte, 31)
+ rightStem := make([]byte, 31)
+ rightStem[0] = 0x80
- node := &InternalNode{
- depth: 0,
- left: leftStem,
- right: rightStem,
+ leftValues := make([][]byte, 256)
+ rightValues := make([][]byte, 256)
+
+ if err := s.InsertValuesAtStem(leftStem, leftValues, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.InsertValuesAtStem(rightStem, rightValues, nil); err != nil {
+ t.Fatal(err)
}
var collectedPaths [][]byte
- var collectedNodes []BinaryNode
-
- flushFn := func(path []byte, n BinaryNode) {
+ flushFn := func(path []byte, hash common.Hash, serialized []byte) {
pathCopy := make([]byte, len(path))
copy(pathCopy, path)
collectedPaths = append(collectedPaths, pathCopy)
- collectedNodes = append(collectedNodes, n)
}
- err := node.CollectNodes([]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(collectedNodes) != 3 {
- t.Errorf("Expected 3 collected nodes, got %d", len(collectedNodes))
- }
-
- // Check paths
- expectedPaths := [][]byte{
- {1, 0}, // left child
- {1, 1}, // right child
- {1}, // internal node itself
- }
-
- for i, expectedPath := range expectedPaths {
- if !bytes.Equal(collectedPaths[i], expectedPath) {
- t.Errorf("Path %d mismatch: expected %v, got %v", i, expectedPath, collectedPaths[i])
- }
+ if len(collectedPaths) != 3 {
+ t.Errorf("Expected 3 collected nodes, got %d", len(collectedPaths))
}
}
-// TestInternalNodeGetHeight tests GetHeight method
+// TestInternalNodeGetHeight tests GetHeight method via nodeStore.
func TestInternalNodeGetHeight(t *testing.T) {
- // Create a tree with different heights
- // Left subtree: depth 2 (internal -> stem)
- // Right subtree: depth 1 (stem)
- leftInternal := &InternalNode{
- depth: 1,
- left: &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 2,
- },
- right: Empty{},
+ s := newNodeStore()
+
+ // Insert values that create a deeper tree
+ stem1 := make([]byte, 31) // left
+ stem2 := make([]byte, 31)
+ stem2[0] = 0x40 // 01... -> goes left at depth 0, right at depth 1
+
+ values1 := make([][]byte, 256)
+ values1[0] = common.HexToHash("0x01").Bytes()
+ values2 := make([][]byte, 256)
+ values2[0] = common.HexToHash("0x02").Bytes()
+
+ if err := s.InsertValuesAtStem(stem1, values1, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.InsertValuesAtStem(stem2, values2, nil); err != nil {
+ t.Fatal(err)
}
- rightStem := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 1,
- }
-
- node := &InternalNode{
- depth: 0,
- left: leftInternal,
- right: rightStem,
- }
-
- height := node.GetHeight()
- // Height should be max(left height, right height) + 1
- // Left height: 2, Right height: 1, so total: 3
- if height != 3 {
- t.Errorf("Expected height 3, got %d", height)
+ height := s.getHeight(s.root)
+ if height < 2 {
+ t.Errorf("Expected height >= 2, got %d", height)
}
}
-// TestInternalNodeDepthTooLarge tests handling of excessive depth
+// TestInternalNodeDepthTooLarge tests handling of excessive depth via nodeStore.
func TestInternalNodeDepthTooLarge(t *testing.T) {
- // Create an internal node at max depth
- node := &InternalNode{
- depth: 31*8 + 1,
- left: Empty{},
- right: Empty{},
- }
-
- stem := make([]byte, 31)
- _, err := node.GetValuesAtStem(stem, nil)
- if err == nil {
- t.Fatal("Expected error for excessive depth")
- }
- if err.Error() != "node too deep" {
- t.Errorf("Expected 'node too deep' error, got: %v", err)
- }
+ s := newNodeStore()
+ // Creating an internal node beyond max depth should panic
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatal("Expected panic for excessive depth")
+ }
+ }()
+ s.newInternalRef(31*8 + 1)
}
diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go
index 048d37f766..a920f91378 100644
--- a/trie/bintrie/iterator.go
+++ b/trie/bintrie/iterator.go
@@ -26,13 +26,14 @@ import (
var errIteratorEnd = errors.New("end of iteration")
type binaryNodeIteratorState struct {
- Node BinaryNode
+ Node nodeRef
Index int
}
type binaryNodeIterator struct {
trie *BinaryTrie
- current BinaryNode
+ store *nodeStore
+ current nodeRef
lastErr error
stack []binaryNodeIteratorState
@@ -40,56 +41,63 @@ type binaryNodeIterator struct {
func newBinaryNodeIterator(t *BinaryTrie, _ []byte) (trie.NodeIterator, error) {
if t.Hash() == zero {
- return &binaryNodeIterator{trie: t, lastErr: errIteratorEnd}, nil
+ return &binaryNodeIterator{trie: t, store: t.store, lastErr: errIteratorEnd}, nil
}
- it := &binaryNodeIterator{trie: t, current: t.root}
- // it.err = it.seek(start)
+ it := &binaryNodeIterator{trie: t, store: t.store, current: t.store.root}
return it, nil
}
-// Next moves the iterator to the next node. If the parameter is false, any child
-// nodes will be skipped.
+// Next moves the iterator to the next node. If descend is false, children of
+// the current node are skipped.
func (it *binaryNodeIterator) Next(descend bool) bool {
if it.lastErr == errIteratorEnd {
- it.lastErr = errIteratorEnd
return false
}
if len(it.stack) == 0 {
- it.stack = append(it.stack, binaryNodeIteratorState{Node: it.trie.root})
- it.current = it.trie.root
-
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: it.trie.store.root})
+ it.current = it.trie.store.root
return true
}
- switch node := it.current.(type) {
- case *InternalNode:
- // index: 0 = nothing visited, 1=left visited, 2=right visited
+ switch it.current.Kind() {
+ case kindInternal:
+ // index: 0 = nothing visited, 1 = left visited, 2 = right visited.
+ node := it.store.getInternal(it.current.Index())
context := &it.stack[len(it.stack)-1]
- // recurse into both children
+ if !descend {
+ // Skip children: pop this node and advance parent.
+ if len(it.stack) == 1 {
+ it.lastErr = errIteratorEnd
+ return false
+ }
+ it.stack = it.stack[:len(it.stack)-1]
+ it.current = it.stack[len(it.stack)-1].Node
+ it.stack[len(it.stack)-1].Index++
+ return it.Next(true)
+ }
+
+ // Recurse into both children.
if context.Index == 0 {
- if _, isempty := node.left.(Empty); node.left != nil && !isempty {
+ if !node.left.IsEmpty() {
it.stack = append(it.stack, binaryNodeIteratorState{Node: node.left})
it.current = node.left
return it.Next(descend)
}
-
context.Index++
}
if context.Index == 1 {
- if _, isempty := node.right.(Empty); node.right != nil && !isempty {
+ if !node.right.IsEmpty() {
it.stack = append(it.stack, binaryNodeIteratorState{Node: node.right})
it.current = node.right
return it.Next(descend)
}
-
context.Index++
}
- // Reached the end of this node, go back to the parent, if
- // this isn't root.
+ // Reached the end of this node; go back to the parent unless we're at the root.
if len(it.stack) == 1 {
it.lastErr = errIteratorEnd
return false
@@ -98,17 +106,18 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
it.current = it.stack[len(it.stack)-1].Node
it.stack[len(it.stack)-1].Index++
return it.Next(descend)
- case *StemNode:
- // Look for the next non-empty value
+
+ case kindStem:
+ // Look for the next non-empty value in this stem.
+ sn := it.store.getStem(it.current.Index())
for i := it.stack[len(it.stack)-1].Index; i < 256; i++ {
- if node.Values[i] != nil {
+ if sn.hasValue(byte(i)) {
it.stack[len(it.stack)-1].Index = i + 1
return true
}
}
- // go back to parent to get the next leaf
- // Check if we're at the root before popping
+ // No more values in this stem; go back to parent to get the next leaf.
if len(it.stack) == 1 {
it.lastErr = errIteratorEnd
return false
@@ -117,51 +126,47 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
it.current = it.stack[len(it.stack)-1].Node
it.stack[len(it.stack)-1].Index++
return it.Next(descend)
- case HashedNode:
- // resolve the node
- resolverPath := it.Path()
- data, err := it.trie.nodeResolver(resolverPath, common.Hash(node))
- if err != nil {
- panic(err)
- }
- if data == nil {
- // Empty/nil node — treat as Empty, backtrack
- it.current = Empty{}
- it.stack[len(it.stack)-1].Node = it.current
- return it.Next(descend)
- }
- it.current, err = DeserializeNodeWithHash(data, len(it.stack)-1, common.Hash(node))
- if err != nil {
- panic(err)
- }
- // update the stack and parent with the resolved node
- it.stack[len(it.stack)-1].Node = it.current
- if len(it.stack) >= 2 {
- parent := &it.stack[len(it.stack)-2]
- if parent.Index == 0 {
- parent.Node.(*InternalNode).left = it.current
- } else {
- parent.Node.(*InternalNode).right = it.current
- }
- }
- return it.Next(descend)
- case Empty:
- // Empty node - go back to parent and continue
- if len(it.stack) <= 1 {
- it.lastErr = errIteratorEnd
+ case kindHashed:
+ // Resolve the hashed node from disk, then rewire the parent to point at the
+ // resolved node in place.
+ if len(it.stack) < 2 {
+ it.lastErr = errors.New("cannot resolve hashed root during iteration")
return false
}
- it.stack = it.stack[:len(it.stack)-1]
- it.current = it.stack[len(it.stack)-1].Node
- it.stack[len(it.stack)-1].Index++
+ hn := it.store.getHashed(it.current.Index())
+ data, err := it.trie.nodeResolver(it.Path(), hn.Hash())
+ if err != nil {
+ it.lastErr = err
+ return false
+ }
+ resolved, err := it.store.deserializeNodeWithHash(data, len(it.stack)-1, hn.Hash())
+ if err != nil {
+ it.lastErr = err
+ return false
+ }
+
+ oldHashedIdx := it.current.Index()
+ it.current = resolved
+ it.stack[len(it.stack)-1].Node = resolved
+ parent := &it.stack[len(it.stack)-2]
+ parentNode := it.store.getInternal(parent.Node.Index())
+ if parent.Index == 0 {
+ parentNode.left = resolved
+ } else {
+ parentNode.right = resolved
+ }
+ it.store.freeHashedNode(oldHashedIdx)
return it.Next(descend)
+
+ case kindEmpty:
+ return false
+
default:
panic("invalid node type")
}
}
-// Error returns the error status of the iterator.
func (it *binaryNodeIterator) Error() error {
if it.lastErr == errIteratorEnd {
return nil
@@ -169,27 +174,28 @@ func (it *binaryNodeIterator) Error() error {
return it.lastErr
}
-// Hash returns the hash of the current node.
func (it *binaryNodeIterator) Hash() common.Hash {
- return it.current.Hash()
+ return it.store.computeHash(it.current)
}
-// Parent returns the hash of the parent of the current node. The hash may be the one
-// grandparent if the immediate parent is an internal node with no hash.
+// Parent returns the hash of the current node's parent. When the immediate
+// parent is an internal node whose hash has not been materialised, the
+// returned hash may be the one of a grandparent instead.
func (it *binaryNodeIterator) Parent() common.Hash {
- return it.stack[len(it.stack)-1].Node.Hash()
+ if len(it.stack) < 2 {
+ return common.Hash{}
+ }
+ return it.store.computeHash(it.stack[len(it.stack)-2].Node)
}
-// Path returns the hex-encoded path to the current node.
-// Callers must not retain references to the return value after calling Next.
-// For leaf nodes, the last element of the path is the 'terminator symbol' 0x10.
+// Path returns the bit-path to the current node.
+// Callers must not retain references to the returned slice after calling Next.
func (it *binaryNodeIterator) Path() []byte {
if it.Leaf() {
return it.LeafKey()
}
var path []byte
for i, state := range it.stack {
- // skip the last byte
if i >= len(it.stack)-1 {
break
}
@@ -198,107 +204,94 @@ func (it *binaryNodeIterator) Path() []byte {
return path
}
-// NodeBlob returns the serialized bytes of the current node.
func (it *binaryNodeIterator) NodeBlob() []byte {
- return SerializeNode(it.current)
+ return it.store.serializeNode(it.current, it.trie.groupDepth)
}
-// Leaf returns true iff the current node is a leaf node.
-// In a Binary Trie, a StemNode contains up to 256 leaf values.
-// The iterator is only considered to be "at a leaf" when it's positioned
-// at a specific non-nil value within the StemNode, not just at the StemNode itself.
+// Leaf reports whether the iterator is currently positioned at a leaf value.
+// A StemNode holds up to 256 values; the iterator is only "at a leaf" when
+// positioned at a specific non-nil value inside the stem, not merely at the
+// StemNode itself. The stack Index points to the NEXT position after the
+// current value, so Index == 0 means we haven't yielded anything yet.
func (it *binaryNodeIterator) Leaf() bool {
- sn, ok := it.current.(*StemNode)
- if !ok {
+ if it.current.Kind() != kindStem {
return false
}
- // Check if we have a valid stack position
if len(it.stack) == 0 {
return false
}
- // The Index in the stack state points to the NEXT position after the current value.
- // So if Index is 0, we haven't started iterating through the values yet.
- // If Index is 5, we're currently at value[4] (the 5th value, 0-indexed).
idx := it.stack[len(it.stack)-1].Index
if idx == 0 || idx > 256 {
return false
}
- // Check if there's actually a value at the current position
+ sn := it.store.getStem(it.current.Index())
currentValueIndex := idx - 1
- return sn.Values[currentValueIndex] != nil
+ return sn.hasValue(byte(currentValueIndex))
}
-// LeafKey returns the key of the leaf. The method panics if the iterator is not
-// positioned at a leaf. Callers must not retain references to the value after
-// calling Next.
+// LeafKey returns the key of the leaf. Panics if the iterator is not
+// positioned at a leaf. Callers must not retain references to the returned
+// slice after calling Next.
func (it *binaryNodeIterator) LeafKey() []byte {
- leaf, ok := it.current.(*StemNode)
- if !ok {
+ if it.current.Kind() != kindStem {
panic("Leaf() called on an binary node iterator not at a leaf location")
}
- return leaf.Key(it.stack[len(it.stack)-1].Index - 1)
+ sn := it.store.getStem(it.current.Index())
+ return sn.Key(it.stack[len(it.stack)-1].Index - 1)
}
-// LeafBlob returns the content of the leaf. The method panics if the iterator
-// is not positioned at a leaf. Callers must not retain references to the value
-// after calling Next.
+// LeafBlob returns the leaf value. Panics if the iterator is not positioned
+// at a leaf. Callers must not retain references to the returned slice after
+// calling Next.
func (it *binaryNodeIterator) LeafBlob() []byte {
- leaf, ok := it.current.(*StemNode)
- if !ok {
+ if it.current.Kind() != kindStem {
panic("LeafBlob() called on an binary node iterator not at a leaf location")
}
- return leaf.Values[it.stack[len(it.stack)-1].Index-1]
+ sn := it.store.getStem(it.current.Index())
+ return sn.getValue(byte(it.stack[len(it.stack)-1].Index - 1))
}
-// LeafProof returns the Merkle proof of the leaf. The method panics if the
-// iterator is not positioned at a leaf. Callers must not retain references
-// to the value after calling Next.
+// LeafProof returns the Merkle proof of the leaf. Panics if the iterator is
+// not positioned at a leaf. Callers must not retain references to the
+// returned slices after calling Next.
func (it *binaryNodeIterator) LeafProof() [][]byte {
- sn, ok := it.current.(*StemNode)
- if !ok {
+ if it.current.Kind() != kindStem {
panic("LeafProof() called on an binary node iterator not at a leaf location")
}
+ sn := it.store.getStem(it.current.Index())
proof := make([][]byte, 0, len(it.stack)+StemNodeWidth)
- // Build proof by walking up the stack and collecting sibling hashes
+ if len(it.stack) < 2 {
+ proof = append(proof, sn.Stem[:])
+ proof = append(proof, sn.allValues()...)
+ return proof
+ }
+
for i := range it.stack[:len(it.stack)-2] {
state := it.stack[i]
- internalNode := state.Node.(*InternalNode) // should panic if the node isn't an InternalNode
+ internalNode := it.store.getInternal(state.Node.Index())
- // Add the sibling hash to the proof
if state.Index == 0 {
- // We came from left, so include right sibling
- proof = append(proof, internalNode.right.Hash().Bytes())
+ rh := it.store.computeHash(internalNode.right)
+ proof = append(proof, rh.Bytes())
} else {
- // We came from right, so include left sibling
- proof = append(proof, internalNode.left.Hash().Bytes())
+ lh := it.store.computeHash(internalNode.left)
+ proof = append(proof, lh.Bytes())
}
}
// Add the stem and siblings
- proof = append(proof, sn.Stem)
- for _, v := range sn.Values {
- proof = append(proof, v)
- }
+ proof = append(proof, sn.Stem[:])
+ proof = append(proof, sn.allValues()...)
return proof
}
-// AddResolver sets an intermediate database to use for looking up trie nodes
-// before reaching into the real persistent layer.
-//
-// This is not required for normal operation, rather is an optimization for
-// cases where trie nodes can be recovered from some external mechanism without
-// reading from disk. In those cases, this resolver allows short circuiting
-// accesses and returning them from memory.
-//
-// Before adding a similar mechanism to any other place in Geth, consider
-// making trie.Database an interface and wrapping at that level. It's a huge
-// refactor, but it could be worth it if another occurrence arises.
+// AddResolver is a no-op (satisfies the NodeIterator interface).
func (it *binaryNodeIterator) AddResolver(trie.NodeResolver) {
// Not implemented, but should not panic
}
diff --git a/trie/bintrie/iterator_test.go b/trie/bintrie/iterator_test.go
index 3e717c07ba..746f6e8c0f 100644
--- a/trie/bintrie/iterator_test.go
+++ b/trie/bintrie/iterator_test.go
@@ -27,14 +27,13 @@ import (
// makeTrie creates a BinaryTrie populated with the given key-value pairs.
func makeTrie(t *testing.T, entries [][2]common.Hash) *BinaryTrie {
t.Helper()
+ store := newNodeStore()
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: store,
tracer: trie.NewPrevalueTracer(),
}
for _, kv := range entries {
- var err error
- tr.root, err = tr.root.Insert(kv[0][:], kv[1][:], nil, 0)
- if err != nil {
+ if err := store.Insert(kv[0][:], kv[1][:], nil); err != nil {
t.Fatal(err)
}
}
@@ -64,7 +63,7 @@ func countLeaves(t *testing.T, tr *BinaryTrie) int {
// no nodes and reports no error.
func TestIteratorEmptyTrie(t *testing.T) {
tr := &BinaryTrie{
- root: Empty{},
+ store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
}
it, err := newBinaryNodeIterator(tr, nil)
@@ -145,8 +144,8 @@ func TestIteratorEmptyNodeBacktrack(t *testing.T) {
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
- if _, ok := tr.root.(*InternalNode); !ok {
- t.Fatalf("expected InternalNode root, got %T", tr.root)
+ if tr.store.root.Kind() != kindInternal {
+ t.Fatalf("expected InternalNode root, got kind %d", tr.store.root.Kind())
}
if leaves := countLeaves(t, tr); leaves != 2 {
t.Fatalf("expected 2 leaves, got %d (Empty backtrack bug?)", leaves)
@@ -162,18 +161,31 @@ func TestIteratorHashedNodeNilData(t *testing.T) {
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
- root, ok := tr.root.(*InternalNode)
- if !ok {
- t.Fatalf("expected InternalNode root, got %T", tr.root)
+ root := tr.store.root
+ if root.Kind() != kindInternal {
+ t.Fatalf("expected InternalNode root, got kind %d", root.Kind())
}
+ rootNode := tr.store.getInternal(root.Index())
// Replace right child with a zero-hash HashedNode. nodeResolver
// short-circuits on common.Hash{} and returns (nil, nil), which
// triggers the nil-data guard in the iterator.
- root.right = HashedNode(common.Hash{})
+ rootNode.right = tr.store.newHashedRef(common.Hash{})
// Should not panic; the zero-hash right child should be treated as Empty.
- if leaves := countLeaves(t, tr); leaves != 1 {
+ // Since the hashed node can't be resolved (nil data -> empty deserialization),
+ // only the left leaf should be counted.
+ it, err := newBinaryNodeIterator(tr, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ leaves := 0
+ for it.Next(true) {
+ if it.Leaf() {
+ leaves++
+ }
+ }
+ if leaves != 1 {
t.Fatalf("expected 1 leaf (zero-hash right node skipped), got %d", leaves)
}
}
diff --git a/trie/bintrie/key_encoding.go b/trie/bintrie/key_encoding.go
index c009f1529f..265935293b 100644
--- a/trie/bintrie/key_encoding.go
+++ b/trie/bintrie/key_encoding.go
@@ -54,15 +54,12 @@ func getBinaryTreeKey(addr common.Address, offset []byte, overflow bool) []byte
defer returnSha256(hasher)
hasher.Write(zeroHash[:12])
hasher.Write(addr[:])
- var buf [32]byte
- // key is big endian, hashed value is little endian
- for i := range offset[:31] {
- buf[i] = offset[30-i]
- }
+ var buf [32]byte // TODO: make offset a 33-byte value to avoid an extra stack alloc
+ copy(buf[1:32], offset[:31])
if overflow {
// Overflow detected when adding MAIN_STORAGE_OFFSET,
// reporting it in the shifter 32 byte value.
- buf[31] = 1
+ buf[0] = 1
}
hasher.Write(buf[:])
k := hasher.Sum(nil)
diff --git a/trie/bintrie/node_ref.go b/trie/bintrie/node_ref.go
new file mode 100644
index 0000000000..1c9c0f6284
--- /dev/null
+++ b/trie/bintrie/node_ref.go
@@ -0,0 +1,56 @@
+// Copyright 2026 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 bintrie
+
+// nodeKind identifies the type of a trie node stored in a nodeRef.
+type nodeKind uint8
+
+const (
+ kindEmpty nodeKind = iota
+ kindInternal
+ kindStem // up to 256 values per stem
+ kindHashed
+)
+
+// nodeRef is a compact, GC-invisible reference to a node in a nodeStore.
+// It packs a 2-bit type tag (bits 31-30) and a 30-bit index (bits 29-0)
+// into a single uint32. Because nodeRef contains no Go pointers, slices
+// of structs containing nodeRef fields are allocated in noscan spans —
+// the garbage collector never examines them.
+type nodeRef uint32
+
+const (
+ kindShift uint32 = 30
+ indexMask uint32 = (1 << kindShift) - 1
+
+ // emptyRef represents an empty node.
+ emptyRef nodeRef = 0
+)
+
+func makeRef(kind nodeKind, idx uint32) nodeRef {
+ if idx > indexMask {
+ panic("nodeRef index overflow")
+ }
+ return nodeRef(uint32(kind)<> kindShift) }
+
+// Index within the typed pool.
+func (r nodeRef) Index() uint32 { return uint32(r) & indexMask }
+
+func (r nodeRef) IsEmpty() bool { return r.Kind() == kindEmpty }
diff --git a/trie/bintrie/node_store.go b/trie/bintrie/node_store.go
new file mode 100644
index 0000000000..8a35f06ee1
--- /dev/null
+++ b/trie/bintrie/node_store.go
@@ -0,0 +1,184 @@
+// Copyright 2026 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 bintrie
+
+import "github.com/ethereum/go-ethereum/common"
+
+// storeChunkSize is the number of nodes per chunk in each typed pool.
+const storeChunkSize = 4096
+
+// nodeStore is a GC-friendly arena for binary trie nodes. Nodes are packed
+// into typed chunked pools so pointer-free types (InternalNode, HashedNode)
+// land in noscan spans the GC skips entirely.
+type nodeStore struct {
+ internalChunks []*[storeChunkSize]InternalNode
+ internalCount uint32
+
+ stemChunks []*[storeChunkSize]StemNode
+ stemCount uint32
+
+ hashedChunks []*[storeChunkSize]HashedNode
+ hashedCount uint32
+
+ root nodeRef
+
+ // Free list for recycling hashed-node slots after resolve. Internal and
+ // stem nodes are never freed under current semantics (no delete path,
+ // stem-split keeps the old stem at a deeper position), so they don't
+ // have free lists.
+ freeHashed []uint32
+}
+
+func newNodeStore() *nodeStore {
+ return &nodeStore{root: emptyRef}
+}
+
+func (s *nodeStore) allocInternal() uint32 {
+ idx := s.internalCount
+ chunkIdx := idx / storeChunkSize
+ if uint32(len(s.internalChunks)) <= chunkIdx {
+ s.internalChunks = append(s.internalChunks, new([storeChunkSize]InternalNode))
+ }
+ s.internalCount++
+ if s.internalCount > indexMask {
+ panic("internal node pool overflow")
+ }
+ return idx
+}
+
+func (s *nodeStore) getInternal(idx uint32) *InternalNode {
+ return &s.internalChunks[idx/storeChunkSize][idx%storeChunkSize]
+}
+
+func (s *nodeStore) newInternalRef(depth int) nodeRef {
+ if depth > 248 {
+ panic("node depth exceeds maximum binary trie depth")
+ }
+ idx := s.allocInternal()
+ n := s.getInternal(idx)
+ n.depth = uint8(depth)
+ n.mustRecompute = true
+ n.dirty = true
+ return makeRef(kindInternal, idx)
+}
+
+func (s *nodeStore) allocStem() uint32 {
+ idx := s.stemCount
+ chunkIdx := idx / storeChunkSize
+ if uint32(len(s.stemChunks)) <= chunkIdx {
+ s.stemChunks = append(s.stemChunks, new([storeChunkSize]StemNode))
+ }
+ s.stemCount++
+ if s.stemCount > indexMask {
+ panic("stem node pool overflow")
+ }
+ return idx
+}
+
+func (s *nodeStore) getStem(idx uint32) *StemNode {
+ return &s.stemChunks[idx/storeChunkSize][idx%storeChunkSize]
+}
+
+func (s *nodeStore) newStemRef(stem []byte, depth int) nodeRef {
+ if depth > 248 {
+ panic("node depth exceeds maximum binary trie depth")
+ }
+ idx := s.allocStem()
+ sn := s.getStem(idx)
+ copy(sn.Stem[:], stem[:StemSize])
+ sn.depth = uint8(depth)
+ sn.mustRecompute = true
+ sn.dirty = true
+ return makeRef(kindStem, idx)
+}
+
+func (s *nodeStore) allocHashed() uint32 {
+ if n := len(s.freeHashed); n > 0 {
+ idx := s.freeHashed[n-1]
+ s.freeHashed = s.freeHashed[:n-1]
+ *s.getHashed(idx) = HashedNode{}
+ return idx
+ }
+ idx := s.hashedCount
+ chunkIdx := idx / storeChunkSize
+ if uint32(len(s.hashedChunks)) <= chunkIdx {
+ s.hashedChunks = append(s.hashedChunks, new([storeChunkSize]HashedNode))
+ }
+ s.hashedCount++
+ if s.hashedCount > indexMask {
+ panic("hashed node pool overflow")
+ }
+ return idx
+}
+
+func (s *nodeStore) getHashed(idx uint32) *HashedNode {
+ return &s.hashedChunks[idx/storeChunkSize][idx%storeChunkSize]
+}
+
+func (s *nodeStore) freeHashedNode(idx uint32) {
+ s.freeHashed = append(s.freeHashed, idx)
+}
+
+func (s *nodeStore) newHashedRef(hash common.Hash) nodeRef {
+ idx := s.allocHashed()
+ *s.getHashed(idx) = HashedNode(hash)
+ return makeRef(kindHashed, idx)
+}
+
+func (s *nodeStore) Copy() *nodeStore {
+ ns := &nodeStore{
+ root: s.root,
+ internalCount: s.internalCount,
+ stemCount: s.stemCount,
+ hashedCount: s.hashedCount,
+ }
+ ns.internalChunks = make([]*[storeChunkSize]InternalNode, len(s.internalChunks))
+ for i, chunk := range s.internalChunks {
+ cp := *chunk
+ ns.internalChunks[i] = &cp
+ }
+ ns.stemChunks = make([]*[storeChunkSize]StemNode, len(s.stemChunks))
+ for i, chunk := range s.stemChunks {
+ cp := *chunk
+ ns.stemChunks[i] = &cp
+ }
+ // Deep-copy each stem's value slots — they may alias serialized buffers,
+ // so we can't rely on the chunk-wise struct copy above.
+ for i := uint32(0); i < s.stemCount; i++ {
+ src := s.getStem(i)
+ dst := ns.getStem(i)
+ for j, v := range src.values {
+ if v == nil {
+ continue
+ }
+ cp := make([]byte, len(v))
+ copy(cp, v)
+ dst.values[j] = cp
+ }
+ }
+ ns.hashedChunks = make([]*[storeChunkSize]HashedNode, len(s.hashedChunks))
+ for i, chunk := range s.hashedChunks {
+ cp := *chunk
+ ns.hashedChunks[i] = &cp
+ }
+ if len(s.freeHashed) > 0 {
+ ns.freeHashed = make([]uint32, len(s.freeHashed))
+ copy(ns.freeHashed, s.freeHashed)
+ }
+
+ return ns
+}
diff --git a/trie/bintrie/stem_node.go b/trie/bintrie/stem_node.go
index e5729e6182..93c55acefa 100644
--- a/trie/bintrie/stem_node.go
+++ b/trie/bintrie/stem_node.go
@@ -17,220 +17,93 @@
package bintrie
import (
- "bytes"
- "errors"
- "fmt"
- "slices"
+ "crypto/sha256"
"github.com/ethereum/go-ethereum/common"
)
-// StemNode represents a group of `NodeWith` values sharing the same stem.
+// StemNode holds up to 256 values sharing a 31-byte stem.
+//
+// Invariant: dirty=false implies mustRecompute=false. Every mutation that
+// invalidates the cached hash MUST also mark the blob for re-flush.
type StemNode struct {
- Stem []byte // Stem path to get to StemNodeWidth values
- Values [][]byte // All values, indexed by the last byte of the key.
- depth int // Depth of the node
+ Stem [StemSize]byte
+ values [StemNodeWidth][]byte // nil == slot absent
- mustRecompute bool // true if the hash needs to be recomputed
+ depth uint8
+
+ mustRecompute bool // hash is stale (cleared by Hash)
+ dirty bool // on-disk blob is stale (cleared by CollectNodes)
hash common.Hash // cached hash when mustRecompute == false
}
-// Get retrieves the value for the given key.
-func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) {
- if !bytes.Equal(bt.Stem, key[:StemSize]) {
- return nil, nil
- }
- return bt.Values[key[StemSize]], nil
+func (sn *StemNode) getValue(suffix byte) []byte {
+ return sn.values[suffix]
}
-// Insert inserts a new key-value pair into the node.
-func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
- if !bytes.Equal(bt.Stem, key[:StemSize]) {
- bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
-
- n := &InternalNode{depth: bt.depth, mustRecompute: true}
- bt.depth++
- var child, other *BinaryNode
- if bitStem == 0 {
- n.left = bt
- child = &n.left
- other = &n.right
- } else {
- n.right = bt
- child = &n.right
- other = &n.left
- }
-
- bitKey := key[n.depth/8] >> (7 - (n.depth % 8)) & 1
- if bitKey == bitStem {
- var err error
- *child, err = (*child).Insert(key, value, nil, depth+1)
- if err != nil {
- return n, fmt.Errorf("insert error: %w", err)
- }
- *other = Empty{}
- } else {
- var values [StemNodeWidth][]byte
- values[key[StemSize]] = value
- *other = &StemNode{
- Stem: slices.Clone(key[:StemSize]),
- Values: values[:],
- depth: depth + 1,
- mustRecompute: true,
- }
- }
- return n, nil
- }
- if len(value) != HashSize {
- return bt, errors.New("invalid insertion: value length")
- }
- bt.Values[key[StemSize]] = value
- bt.mustRecompute = true
- return bt, nil
+func (sn *StemNode) hasValue(suffix byte) bool {
+ return sn.values[suffix] != nil
}
-// Copy creates a deep copy of the node.
-func (bt *StemNode) Copy() BinaryNode {
- var values [StemNodeWidth][]byte
- for i, v := range bt.Values {
- values[i] = slices.Clone(v)
- }
- return &StemNode{
- Stem: slices.Clone(bt.Stem),
- Values: values[:],
- depth: bt.depth,
- hash: bt.hash,
- mustRecompute: bt.mustRecompute,
- }
+// allValues returns the underlying slot array as a slice. nil entries mean
+// absent. Callers must treat it as read-only.
+func (sn *StemNode) allValues() [][]byte {
+ return sn.values[:]
}
-// GetHeight returns the height of the node.
-func (bt *StemNode) GetHeight() int {
- return 1
+// setValue mutates a value slot and marks the stem for re-hash and
+// re-flush. This is the only API for post-load value mutation; direct
+// values[...] writes are reserved for the on-disk load path in
+// decodeNode, which must leave mustRecompute/dirty at their loaded
+// state.
+func (sn *StemNode) setValue(suffix byte, value []byte) {
+ sn.values[suffix] = value
+ sn.mustRecompute = true
+ sn.dirty = true
}
-// Hash returns the hash of the node.
-func (bt *StemNode) Hash() common.Hash {
- if !bt.mustRecompute {
- return bt.hash
+func (sn *StemNode) Hash() common.Hash {
+ if !sn.mustRecompute {
+ return sn.hash
}
+ // Use sha256.Sum256 (returns [32]byte by value) instead of a pooled
+ // hash.Hash: feeding data[i][:0] into the interface method Sum forces
+ // data to heap (escape analysis is conservative through interfaces).
+ // Sum256 takes []byte and returns by value, so data stays on stack.
var data [StemNodeWidth]common.Hash
- h := newSha256()
- defer returnSha256(h)
- for i, v := range bt.Values {
+
+ for i, v := range sn.values {
if v != nil {
- h.Reset()
- h.Write(v)
- h.Sum(data[i][:0])
+ data[i] = sha256.Sum256(v)
}
}
- h.Reset()
+ var pair [2 * HashSize]byte
for level := 1; level <= 8; level++ {
for i := range StemNodeWidth / (1 << level) {
- h.Reset()
-
if data[i*2] == (common.Hash{}) && data[i*2+1] == (common.Hash{}) {
data[i] = common.Hash{}
continue
}
-
- h.Write(data[i*2][:])
- h.Write(data[i*2+1][:])
- data[i] = common.Hash(h.Sum(nil))
+ copy(pair[:HashSize], data[i*2][:])
+ copy(pair[HashSize:], data[i*2+1][:])
+ data[i] = sha256.Sum256(pair[:])
}
}
- h.Reset()
- h.Write(bt.Stem)
- h.Write([]byte{0})
- h.Write(data[0][:])
- bt.hash = common.BytesToHash(h.Sum(nil))
- bt.mustRecompute = false
- return bt.hash
+ var final [StemSize + 1 + HashSize]byte
+ copy(final[:StemSize], sn.Stem[:])
+ final[StemSize] = 0
+ copy(final[StemSize+1:], data[0][:])
+ sn.hash = sha256.Sum256(final[:])
+ sn.mustRecompute = false
+ return sn.hash
}
-// CollectNodes collects all child nodes at a given path, and flushes it
-// into the provided node collector.
-func (bt *StemNode) CollectNodes(path []byte, flush NodeFlushFn) error {
- flush(path, bt)
- return nil
-}
-
-// GetValuesAtStem retrieves the group of values located at the given stem key.
-func (bt *StemNode) GetValuesAtStem(stem []byte, _ NodeResolverFn) ([][]byte, error) {
- if !bytes.Equal(bt.Stem, stem) {
- return nil, nil
- }
- return bt.Values[:], nil
-}
-
-// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
-// Already-existing values will be overwritten.
-func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
- if !bytes.Equal(bt.Stem, key[:StemSize]) {
- bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
-
- n := &InternalNode{depth: bt.depth, mustRecompute: true}
- bt.depth++
- var child, other *BinaryNode
- if bitStem == 0 {
- n.left = bt
- child = &n.left
- other = &n.right
- } else {
- n.right = bt
- child = &n.right
- other = &n.left
- }
-
- bitKey := key[n.depth/8] >> (7 - (n.depth % 8)) & 1
- if bitKey == bitStem {
- var err error
- *child, err = (*child).InsertValuesAtStem(key, values, nil, depth+1)
- if err != nil {
- return n, fmt.Errorf("insert error: %w", err)
- }
- *other = Empty{}
- } else {
- *other = &StemNode{
- Stem: slices.Clone(key[:StemSize]),
- Values: values,
- depth: n.depth + 1,
- mustRecompute: true,
- }
- }
- return n, nil
- }
-
- // same stem, just merge the two value lists
- for i, v := range values {
- if v != nil {
- bt.Values[i] = v
- bt.mustRecompute = true
- }
- }
- return bt, nil
-}
-
-func (bt *StemNode) toDot(parent, path string) string {
- me := fmt.Sprintf("stem%s", path)
- ret := fmt.Sprintf("%s [label=\"stem=%x c=%x\"]\n", me, bt.Stem, bt.Hash())
- ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
- for i, v := range bt.Values {
- if v != nil {
- ret = fmt.Sprintf("%s%s%x [label=\"%x\"]\n", ret, me, i, v)
- ret = fmt.Sprintf("%s%s -> %s%x\n", ret, me, me, i)
- }
- }
- return ret
-}
-
-// Key returns the full key for the given index.
-func (bt *StemNode) Key(i int) []byte {
+func (sn *StemNode) Key(i int) []byte {
var ret [HashSize]byte
- copy(ret[:], bt.Stem)
+ copy(ret[:], sn.Stem[:])
ret[StemSize] = byte(i)
return ret[:]
}
diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go
index 310c553d39..ae6b57ab34 100644
--- a/trie/bintrie/stem_node_test.go
+++ b/trie/bintrie/stem_node_test.go
@@ -23,165 +23,99 @@ import (
"github.com/ethereum/go-ethereum/common"
)
-// TestStemNodeGet tests the Get method for matching stem, non-matching stem,
-// and nil-value suffix scenarios.
-func TestStemNodeGet(t *testing.T) {
- stem := make([]byte, StemSize)
- stem[0] = 0xAB
- var values [StemNodeWidth][]byte
- values[5] = common.HexToHash("0xdeadbeef").Bytes()
-
- node := &StemNode{Stem: stem, Values: values[:], depth: 0}
-
- // Matching stem, populated suffix → returns value.
- key := make([]byte, HashSize)
- copy(key[:StemSize], stem)
- key[StemSize] = 5
- got, err := node.Get(key, nil)
- if err != nil {
- t.Fatalf("Get error: %v", err)
- }
- if !bytes.Equal(got, values[5]) {
- t.Fatalf("Get = %x, want %x", got, values[5])
- }
-
- // Matching stem, empty suffix → returns nil (slot not set).
- key[StemSize] = 99
- got, err = node.Get(key, nil)
- if err != nil {
- t.Fatalf("Get error: %v", err)
- }
- if got != nil {
- t.Fatalf("Get(empty suffix) = %x, want nil", got)
- }
-
- // Non-matching stem → returns nil, nil.
- otherKey := make([]byte, HashSize)
- otherKey[0] = 0xFF
- got, err = node.Get(otherKey, nil)
- if err != nil {
- t.Fatalf("Get error: %v", err)
- }
- if got != nil {
- t.Fatalf("Get(wrong stem) = %x, want nil", got)
- }
-}
-
-// TestStemNodeInsertSameStem tests inserting values with the same stem
+// TestStemNodeInsertSameStem tests inserting values with the same stem via nodeStore.
func TestStemNodeInsertSameStem(t *testing.T) {
+ s := newNodeStore()
+
stem := make([]byte, 31)
for i := range stem {
stem[i] = byte(i)
}
- var values [256][]byte
- values[0] = common.HexToHash("0x0101").Bytes()
-
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
+ // Insert first value
+ key1 := make([]byte, 32)
+ copy(key1[:31], stem)
+ key1[31] = 0
+ value1 := common.HexToHash("0x0101").Bytes()
+ if err := s.Insert(key1, value1, nil); err != nil {
+ t.Fatal(err)
}
// Insert another value with the same stem but different last byte
- key := make([]byte, 32)
- copy(key[:31], stem)
- key[31] = 10
- value := common.HexToHash("0x0202").Bytes()
-
- newNode, err := node.Insert(key, value, nil, 0)
- if err != nil {
- t.Fatalf("Failed to insert: %v", err)
+ key2 := make([]byte, 32)
+ copy(key2[:31], stem)
+ key2[31] = 10
+ value2 := common.HexToHash("0x0202").Bytes()
+ if err := s.Insert(key2, value2, nil); err != nil {
+ t.Fatal(err)
}
- // Should still be a StemNode
- stemNode, ok := newNode.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", newNode)
+ // Root should still be a StemNode
+ if s.root.Kind() != kindStem {
+ t.Fatalf("Expected kindStem root, got kind %d", s.root.Kind())
}
// Check that both values are present
- if !bytes.Equal(stemNode.Values[0], values[0]) {
+ v1, _ := s.Get(key1, nil)
+ if !bytes.Equal(v1, value1) {
t.Errorf("Value at index 0 mismatch")
}
- if !bytes.Equal(stemNode.Values[10], value) {
+ v2, _ := s.Get(key2, nil)
+ if !bytes.Equal(v2, value2) {
t.Errorf("Value at index 10 mismatch")
}
}
-// TestStemNodeInsertDifferentStem tests inserting values with different stems
+// TestStemNodeInsertDifferentStem tests inserting values with different stems via nodeStore.
func TestStemNodeInsertDifferentStem(t *testing.T) {
- stem1 := make([]byte, 31)
- for i := range stem1 {
- stem1[i] = 0x00
- }
+ s := newNodeStore()
- var values [256][]byte
- values[0] = common.HexToHash("0x0101").Bytes()
-
- node := &StemNode{
- Stem: stem1,
- Values: values[:],
- depth: 0,
+ // Insert first value with stem of all zeros
+ key1 := make([]byte, 32)
+ key1[31] = 0
+ value1 := common.HexToHash("0x0101").Bytes()
+ if err := s.Insert(key1, value1, nil); err != nil {
+ t.Fatal(err)
}
// Insert with a different stem (first bit different)
- key := make([]byte, 32)
- key[0] = 0x80 // First bit is 1 instead of 0
- value := common.HexToHash("0x0202").Bytes()
-
- newNode, err := node.Insert(key, value, nil, 0)
- if err != nil {
- t.Fatalf("Failed to insert: %v", err)
+ key2 := make([]byte, 32)
+ key2[0] = 0x80 // First bit is 1 instead of 0
+ value2 := common.HexToHash("0x0202").Bytes()
+ if err := s.Insert(key2, value2, nil); err != nil {
+ t.Fatal(err)
}
// Should now be an InternalNode
- internalNode, ok := newNode.(*InternalNode)
- if !ok {
- t.Fatalf("Expected InternalNode, got %T", newNode)
+ if s.root.Kind() != kindInternal {
+ t.Fatalf("Expected kindInternal root, got kind %d", s.root.Kind())
}
// Check depth
- if internalNode.depth != 0 {
- t.Errorf("Expected depth 0, got %d", internalNode.depth)
+ rootNode := s.getInternal(s.root.Index())
+ if rootNode.depth != 0 {
+ t.Errorf("Expected depth 0, got %d", rootNode.depth)
}
- // Original stem should be on the left (bit 0)
- leftStem, ok := internalNode.left.(*StemNode)
- if !ok {
- t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
+ // Verify both values are retrievable
+ v1, _ := s.Get(key1, nil)
+ if !bytes.Equal(v1, value1) {
+ t.Error("Value 1 mismatch")
}
- if !bytes.Equal(leftStem.Stem, stem1) {
- t.Errorf("Left stem mismatch")
- }
-
- // New stem should be on the right (bit 1)
- rightStem, ok := internalNode.right.(*StemNode)
- if !ok {
- t.Fatalf("Expected right child to be StemNode, got %T", internalNode.right)
- }
- if !bytes.Equal(rightStem.Stem, key[:31]) {
- t.Errorf("Right stem mismatch")
+ v2, _ := s.Get(key2, nil)
+ if !bytes.Equal(v2, value2) {
+ t.Error("Value 2 mismatch")
}
}
-// TestStemNodeInsertInvalidValueLength tests inserting value with invalid length
+// TestStemNodeInsertInvalidValueLength tests inserting value with invalid length via nodeStore.
func TestStemNodeInsertInvalidValueLength(t *testing.T) {
- stem := make([]byte, 31)
- var values [256][]byte
+ s := newNodeStore()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
- }
-
- // Try to insert value with wrong length
key := make([]byte, 32)
- copy(key[:31], stem)
invalidValue := []byte{1, 2, 3} // Not 32 bytes
- _, err := node.Insert(key, invalidValue, nil, 0)
+ err := s.Insert(key, invalidValue, nil)
if err == nil {
t.Fatal("Expected error for invalid value length")
}
@@ -191,220 +125,206 @@ func TestStemNodeInsertInvalidValueLength(t *testing.T) {
}
}
-// TestStemNodeCopy tests the Copy method
+// TestStemNodeCopy tests the Copy method via nodeStore.
func TestStemNodeCopy(t *testing.T) {
- stem := make([]byte, 31)
- for i := range stem {
- stem[i] = byte(i)
+ s := newNodeStore()
+
+ key1 := make([]byte, 32)
+ for i := range 31 {
+ key1[i] = byte(i)
+ }
+ key1[31] = 0
+ value1 := common.HexToHash("0x0101").Bytes()
+
+ key2 := make([]byte, 32)
+ copy(key2[:31], key1[:31])
+ key2[31] = 255
+ value2 := common.HexToHash("0x0202").Bytes()
+
+ if err := s.Insert(key1, value1, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Insert(key2, value2, nil); err != nil {
+ t.Fatal(err)
}
- var values [256][]byte
- values[0] = common.HexToHash("0x0101").Bytes()
- values[255] = common.HexToHash("0x0202").Bytes()
+ ns := s.Copy()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 10,
- }
-
- // Create a copy
- copied := node.Copy()
- copiedStem, ok := copied.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", copied)
- }
-
- // Check that values are equal but not the same slice
- if !bytes.Equal(copiedStem.Stem, node.Stem) {
- t.Errorf("Stem mismatch after copy")
- }
- if &copiedStem.Stem[0] == &node.Stem[0] {
- t.Error("Stem slice not properly cloned")
- }
-
- // Check values
- if !bytes.Equal(copiedStem.Values[0], node.Values[0]) {
+ // Check that values are equal
+ v1, _ := ns.Get(key1, nil)
+ if !bytes.Equal(v1, value1) {
t.Errorf("Value at index 0 mismatch after copy")
}
- if !bytes.Equal(copiedStem.Values[255], node.Values[255]) {
+ v2, _ := ns.Get(key2, nil)
+ if !bytes.Equal(v2, value2) {
t.Errorf("Value at index 255 mismatch after copy")
}
-
- // Check that value slices are cloned
- if copiedStem.Values[0] != nil && &copiedStem.Values[0][0] == &node.Values[0][0] {
- t.Error("Value slice not properly cloned")
- }
-
- // Check depth
- if copiedStem.depth != node.depth {
- t.Errorf("Depth mismatch: expected %d, got %d", node.depth, copiedStem.depth)
- }
}
-// TestStemNodeHash tests the Hash method
+// TestStemNodeHash tests the Hash method.
func TestStemNodeHash(t *testing.T) {
- stem := make([]byte, 31)
- var values [256][]byte
- values[0] = common.HexToHash("0x0101").Bytes()
+ s := newNodeStore()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
+ key := make([]byte, 32)
+ key[31] = 0
+ value := common.HexToHash("0x0101").Bytes()
+ if err := s.Insert(key, value, nil); err != nil {
+ t.Fatal(err)
}
- hash1 := node.Hash()
+ hash1 := s.computeHash(s.root)
// Hash should be deterministic
- hash2 := node.Hash()
+ hash2 := s.computeHash(s.root)
if hash1 != hash2 {
t.Errorf("Hash not deterministic: %x != %x", hash1, hash2)
}
// Changing a value should change the hash
- node.Values[1] = common.HexToHash("0x0202").Bytes()
- node.mustRecompute = true
- hash3 := node.Hash()
+ key2 := make([]byte, 32)
+ key2[31] = 1
+ value2 := common.HexToHash("0x0202").Bytes()
+ if err := s.Insert(key2, value2, nil); err != nil {
+ t.Fatal(err)
+ }
+ hash3 := s.computeHash(s.root)
if hash1 == hash3 {
t.Error("Hash didn't change after modifying values")
}
}
-// TestStemNodeGetValuesAtStem tests GetValuesAtStem method
+// TestStemNodeGetValuesAtStem tests GetValuesAtStem method via nodeStore.
func TestStemNodeGetValuesAtStem(t *testing.T) {
+ s := newNodeStore()
+
stem := make([]byte, 31)
for i := range stem {
stem[i] = byte(i)
}
- var values [256][]byte
+ values := make([][]byte, 256)
values[0] = common.HexToHash("0x0101").Bytes()
values[10] = common.HexToHash("0x0202").Bytes()
values[255] = common.HexToHash("0x0303").Bytes()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
+ if err := s.InsertValuesAtStem(stem, values, nil); err != nil {
+ t.Fatal(err)
}
// GetValuesAtStem with matching stem
- retrievedValues, err := node.GetValuesAtStem(stem, nil)
+ retrievedValues, err := s.GetValuesAtStem(stem, nil)
if err != nil {
t.Fatalf("Failed to get values: %v", err)
}
- // Check that all values match
- for i := range 256 {
- if !bytes.Equal(retrievedValues[i], values[i]) {
- t.Errorf("Value mismatch at index %d", i)
- }
+ if !bytes.Equal(retrievedValues[0], values[0]) {
+ t.Error("Value at index 0 mismatch")
+ }
+ if !bytes.Equal(retrievedValues[10], values[10]) {
+ t.Error("Value at index 10 mismatch")
+ }
+ if !bytes.Equal(retrievedValues[255], values[255]) {
+ t.Error("Value at index 255 mismatch")
}
- // GetValuesAtStem with different stem should return nil
+ // GetValuesAtStem with different stem should return nil values
differentStem := make([]byte, 31)
differentStem[0] = 0xFF
- shouldBeNil, err := node.GetValuesAtStem(differentStem, nil)
+ shouldBeEmpty, err := s.GetValuesAtStem(differentStem, nil)
if err != nil {
t.Fatalf("Failed to get values with different stem: %v", err)
}
- if shouldBeNil != nil {
- t.Error("Expected nil for different stem, got non-nil")
+ allNil := true
+ for _, v := range shouldBeEmpty {
+ if v != nil {
+ allNil = false
+ break
+ }
+ }
+ if !allNil {
+ t.Error("Expected all nil values for different stem")
}
}
-// TestStemNodeInsertValuesAtStem tests InsertValuesAtStem method
+// TestStemNodeInsertValuesAtStem tests InsertValuesAtStem method via nodeStore.
func TestStemNodeInsertValuesAtStem(t *testing.T) {
+ s := newNodeStore()
+
stem := make([]byte, 31)
- var values [256][]byte
+ values := make([][]byte, 256)
values[0] = common.HexToHash("0x0101").Bytes()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
+ if err := s.InsertValuesAtStem(stem, values, nil); err != nil {
+ t.Fatal(err)
}
// Insert new values at the same stem
- var newValues [256][]byte
+ newValues := make([][]byte, 256)
newValues[1] = common.HexToHash("0x0202").Bytes()
newValues[2] = common.HexToHash("0x0303").Bytes()
- newNode, err := node.InsertValuesAtStem(stem, newValues[:], nil, 0)
- if err != nil {
- t.Fatalf("Failed to insert values: %v", err)
- }
-
- stemNode, ok := newNode.(*StemNode)
- if !ok {
- t.Fatalf("Expected StemNode, got %T", newNode)
+ if err := s.InsertValuesAtStem(stem, newValues, nil); err != nil {
+ t.Fatal(err)
}
// Check that all values are present
- if !bytes.Equal(stemNode.Values[0], values[0]) {
+ retrieved, err := s.GetValuesAtStem(stem, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(retrieved[0], values[0]) {
t.Error("Original value at index 0 missing")
}
- if !bytes.Equal(stemNode.Values[1], newValues[1]) {
+ if !bytes.Equal(retrieved[1], newValues[1]) {
t.Error("New value at index 1 missing")
}
- if !bytes.Equal(stemNode.Values[2], newValues[2]) {
+ if !bytes.Equal(retrieved[2], newValues[2]) {
t.Error("New value at index 2 missing")
}
}
-// TestStemNodeGetHeight tests GetHeight method
+// TestStemNodeGetHeight tests GetHeight method via nodeStore.
func TestStemNodeGetHeight(t *testing.T) {
- node := &StemNode{
- Stem: make([]byte, 31),
- Values: make([][]byte, 256),
- depth: 0,
+ s := newNodeStore()
+
+ key := make([]byte, 32)
+ value := common.HexToHash("0x01").Bytes()
+ if err := s.Insert(key, value, nil); err != nil {
+ t.Fatal(err)
}
- height := node.GetHeight()
+ height := s.getHeight(s.root)
if height != 1 {
t.Errorf("Expected height 1, got %d", height)
}
}
-// TestStemNodeCollectNodes tests CollectNodes method
+// TestStemNodeCollectNodes tests CollectNodes method via nodeStore.
func TestStemNodeCollectNodes(t *testing.T) {
+ s := newNodeStore()
+
stem := make([]byte, 31)
- var values [256][]byte
+ values := make([][]byte, 256)
values[0] = common.HexToHash("0x0101").Bytes()
- node := &StemNode{
- Stem: stem,
- Values: values[:],
- depth: 0,
+ if err := s.InsertValuesAtStem(stem, values, nil); err != nil {
+ t.Fatal(err)
}
var collectedPaths [][]byte
- var collectedNodes []BinaryNode
-
- flushFn := func(path []byte, n BinaryNode) {
- // Make a copy of the path
+ flushFn := func(path []byte, hash common.Hash, serialized []byte) {
pathCopy := make([]byte, len(path))
copy(pathCopy, path)
collectedPaths = append(collectedPaths, pathCopy)
- collectedNodes = append(collectedNodes, n)
}
- err := node.CollectNodes([]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(collectedNodes) != 1 {
- t.Errorf("Expected 1 collected node, got %d", len(collectedNodes))
- }
-
- // Check that the collected node is the same
- if collectedNodes[0] != node {
- t.Error("Collected node doesn't match original")
+ if len(collectedPaths) != 1 {
+ t.Errorf("Expected 1 collected node, got %d", len(collectedPaths))
}
// Check the path
diff --git a/trie/bintrie/store_commit.go b/trie/bintrie/store_commit.go
new file mode 100644
index 0000000000..b14bffbc6c
--- /dev/null
+++ b/trie/bintrie/store_commit.go
@@ -0,0 +1,487 @@
+// Copyright 2026 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 bintrie
+
+import (
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "math/bits"
+ "runtime"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type nodeFlushFn func(path []byte, hash common.Hash, serialized []byte)
+
+func (s *nodeStore) Hash() common.Hash {
+ return s.computeHash(s.root)
+}
+
+func (s *nodeStore) computeHash(ref nodeRef) common.Hash {
+ switch ref.Kind() {
+ case kindInternal:
+ return s.hashInternal(ref.Index())
+ case kindStem:
+ return s.getStem(ref.Index()).Hash()
+ case kindHashed:
+ return s.getHashed(ref.Index()).Hash()
+ case kindEmpty:
+ return common.Hash{}
+ default:
+ return common.Hash{}
+ }
+}
+
+// parallelHashDepth is the tree depth below which hashInternal spawns
+// goroutines for shallow-depth parallelism. Computed once at init because
+// NumCPU() never changes after startup.
+var parallelHashDepth = min(bits.Len(uint(runtime.NumCPU())), 8)
+
+// hashInternal hashes an InternalNode and caches the result.
+//
+// At shallow depths (< parallelHashDepth) the left subtree is hashed in a
+// goroutine while the right subtree is hashed inline, then the two digests
+// are combined. Below that threshold the goroutine spawn cost outweighs the
+// hashing work, so deeper nodes hash both children sequentially.
+func (s *nodeStore) hashInternal(idx uint32) common.Hash {
+ node := s.getInternal(idx)
+ if !node.mustRecompute {
+ return node.hash
+ }
+
+ if int(node.depth) < parallelHashDepth {
+ var input [64]byte
+ var lh common.Hash
+ var wg sync.WaitGroup
+ if !node.left.IsEmpty() {
+ wg.Add(1)
+ go func() {
+ // defer wg.Done() so a panic in computeHash still releases
+ // the waiter; without this, a recover() higher in the call
+ // stack would leave the parent stuck in wg.Wait forever.
+ defer wg.Done()
+ lh = s.computeHash(node.left)
+ }()
+ }
+ if !node.right.IsEmpty() {
+ rh := s.computeHash(node.right)
+ copy(input[32:], rh[:])
+ }
+ wg.Wait()
+ copy(input[:32], lh[:])
+ node.hash = sha256.Sum256(input[:])
+ node.mustRecompute = false
+ return node.hash
+ }
+
+ // Deep sequential branch — mirrors the shallow branch's shape to keep
+ // input on the stack. Writing lh/rh through hash.Hash (interface)
+ // forces escape; copy into a local [64]byte and hash it in one shot.
+ var input [64]byte
+ if !node.left.IsEmpty() {
+ lh := s.computeHash(node.left)
+ copy(input[:HashSize], lh[:])
+ }
+ if !node.right.IsEmpty() {
+ rh := s.computeHash(node.right)
+ copy(input[HashSize:], rh[:])
+ }
+ node.hash = sha256.Sum256(input[:])
+ node.mustRecompute = false
+ return node.hash
+}
+
+// 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())
+ 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
+ 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())
+ // Count present slots to size the blob.
+ var count int
+ for _, v := range sn.values {
+ if v != nil {
+ count++
+ }
+ }
+ serializedLen := NodeTypeBytes + StemSize + StemBitmapSize + count*HashSize
+ serialized := make([]byte, serializedLen)
+ serialized[0] = nodeTypeStem
+ copy(serialized[NodeTypeBytes:NodeTypeBytes+StemSize], sn.Stem[:])
+ bitmap := serialized[NodeTypeBytes+StemSize : NodeTypeBytes+StemSize+StemBitmapSize]
+ offset := NodeTypeBytes + StemSize + StemBitmapSize
+ for i, v := range sn.values {
+ if v != nil {
+ bitmap[i/8] |= 1 << (7 - (i % 8))
+ copy(serialized[offset:offset+HashSize], v)
+ offset += HashSize
+ }
+ }
+ return serialized
+
+ default:
+ panic(fmt.Sprintf("SerializeNode: unexpected node kind %d", ref.Kind()))
+ }
+}
+
+var errInvalidSerializedLength = errors.New("invalid serialized node length")
+
+// DeserializeNode deserializes a node from bytes, recomputing its hash. The
+// returned node is marked dirty (provenance unknown, safe re-flush default).
+func (s *nodeStore) deserializeNode(serialized []byte, depth int) (nodeRef, error) {
+ return s.decodeNode(serialized, depth, common.Hash{}, true, true)
+}
+
+// DeserializeNodeWithHash deserializes a node whose hash is already known and
+// whose blob is already on disk (mustRecompute=false, dirty=false).
+func (s *nodeStore) deserializeNodeWithHash(serialized []byte, depth int, hn common.Hash) (nodeRef, error) {
+ 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
+ }
+
+ switch serialized[0] {
+ case nodeTypeInternal:
+ // Grouped format: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes
+ if len(serialized) < NodeTypeBytes+1 {
+ return emptyRef, errInvalidSerializedLength
+ }
+ 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:]
+
+ hashIdx := 0
+ return s.deserializeSubtree(hn, groupDepth, 0, depth, bitmap, hashData, &hashIdx, mustRecompute, dirty)
+
+ case nodeTypeStem:
+ if len(serialized) < NodeTypeBytes+StemSize+StemBitmapSize {
+ return emptyRef, errInvalidSerializedLength
+ }
+ stemIdx := s.allocStem()
+ sn := s.getStem(stemIdx)
+ copy(sn.Stem[:], serialized[NodeTypeBytes:NodeTypeBytes+StemSize])
+ bitmap := serialized[NodeTypeBytes+StemSize : NodeTypeBytes+StemSize+StemBitmapSize]
+ offset := NodeTypeBytes + StemSize + StemBitmapSize
+ for i := range StemNodeWidth {
+ if bitmap[i/8]>>(7-(i%8))&1 != 1 {
+ continue
+ }
+ if len(serialized) < offset+HashSize {
+ return emptyRef, errInvalidSerializedLength
+ }
+ // Zero-copy: each slot aliases the serialized input buffer.
+ sn.values[i] = serialized[offset : offset+HashSize]
+ offset += HashSize
+ }
+ sn.depth = uint8(depth)
+ sn.hash = hn
+ sn.mustRecompute = mustRecompute
+ sn.dirty = dirty
+ return makeRef(kindStem, stemIdx), nil
+
+ default:
+ return emptyRef, errors.New("invalid node type")
+ }
+}
+
+// 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, groupDepth int) {
+ switch ref.Kind() {
+ case kindInternal:
+ node := s.getInternal(ref.Index())
+ if !node.dirty {
+ return
+ }
+ // 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
+ }
+ // 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
+ }
+ flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth))
+ sn.dirty = false
+ case kindHashed, kindEmpty:
+ default:
+ 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:
+ node := s.getInternal(ref.Index())
+ me := fmt.Sprintf("internal%s", path)
+ ret := fmt.Sprintf("%s [label=\"I: %x\"]\n", me, s.computeHash(ref))
+ if len(parent) > 0 {
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ }
+ if !node.left.IsEmpty() {
+ ret += s.toDot(node.left, me, fmt.Sprintf("%s%b", path, 0))
+ }
+ if !node.right.IsEmpty() {
+ ret += s.toDot(node.right, me, fmt.Sprintf("%s%b", path, 1))
+ }
+ return ret
+ case kindStem:
+ sn := s.getStem(ref.Index())
+ me := fmt.Sprintf("stem%s", path)
+ ret := fmt.Sprintf("%s [label=\"stem=%x c=%x\"]\n", me, sn.Stem, sn.Hash())
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ for i, v := range sn.values {
+ if v == nil {
+ continue
+ }
+ ret += fmt.Sprintf("%s%x [label=\"%x\"]\n", me, i, v)
+ ret += fmt.Sprintf("%s -> %s%x\n", me, me, i)
+ }
+ return ret
+ case kindHashed:
+ hn := s.getHashed(ref.Index())
+ me := fmt.Sprintf("hash%s", path)
+ ret := fmt.Sprintf("%s [label=\"%x\"]\n", me, hn.Hash())
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ return ret
+ default:
+ return ""
+ }
+}
diff --git a/trie/bintrie/store_ops.go b/trie/bintrie/store_ops.go
new file mode 100644
index 0000000000..9a73c8bd64
--- /dev/null
+++ b/trie/bintrie/store_ops.go
@@ -0,0 +1,345 @@
+// Copyright 2026 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 bintrie
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// nodeResolverFn resolves a hashed node from the database.
+type nodeResolverFn func([]byte, common.Hash) ([]byte, error)
+
+// GetValue returns the value at (stem, suffix) or nil if absent. Thin
+// wrapper over GetValuesAtStem — the underlying StemNode returns its
+// 256-slot array as a slice header (no allocation), so the per-call cost
+// is the tree walk plus one index.
+func (s *nodeStore) GetValue(stem []byte, suffix byte, resolver nodeResolverFn) ([]byte, error) {
+ values, err := s.GetValuesAtStem(stem, resolver)
+ if err != nil || values == nil {
+ return nil, err
+ }
+ return values[suffix], nil
+}
+
+// GetValuesAtStem returns the 256 value slots at stem, or nil if the stem
+// is not in the trie. The returned slice is a view over the in-place
+// StemNode values array (no allocation) and must be treated read-only.
+func (s *nodeStore) GetValuesAtStem(stem []byte, resolver nodeResolverFn) ([][]byte, error) {
+ cur := s.root
+ var parentIdx uint32
+ var parentIsLeft bool
+
+ for {
+ switch cur.Kind() {
+ case kindInternal:
+ node := s.getInternal(cur.Index())
+ if node.depth >= 31*8 {
+ return nil, errors.New("node too deep")
+ }
+ bit := stem[node.depth/8] >> (7 - (node.depth % 8)) & 1
+ parentIdx = cur.Index()
+ if bit == 0 {
+ parentIsLeft = true
+ cur = node.left
+ } else {
+ parentIsLeft = false
+ cur = node.right
+ }
+
+ case kindStem:
+ sn := s.getStem(cur.Index())
+ if sn.Stem != [StemSize]byte(stem[:StemSize]) {
+ return nil, nil
+ }
+ return sn.allValues(), nil
+
+ case kindHashed:
+ // HashedNode at root is impossible: NewBinaryTrie resolves the
+ // root eagerly before any query. Any HashedNode we encounter here
+ // is necessarily a child of a previously-visited internal node.
+ if resolver == nil {
+ return nil, errors.New("getValuesAtStem: cannot resolve hashed node without resolver")
+ }
+ hn := s.getHashed(cur.Index())
+ parentNode := s.getInternal(parentIdx)
+ path, err := keyToPath(int(parentNode.depth), stem)
+ if err != nil {
+ return nil, fmt.Errorf("getValuesAtStem path error: %w", err)
+ }
+ data, err := resolver(path, hn.Hash())
+ if err != nil {
+ return nil, fmt.Errorf("getValuesAtStem resolve error: %w", err)
+ }
+ resolved, err := s.deserializeNodeWithHash(data, int(parentNode.depth)+1, hn.Hash())
+ if err != nil {
+ return nil, fmt.Errorf("getValuesAtStem deserialization error: %w", err)
+ }
+ s.freeHashedNode(cur.Index())
+ if parentIsLeft {
+ parentNode.left = resolved
+ } else {
+ parentNode.right = resolved
+ }
+ cur = resolved
+
+ case kindEmpty:
+ var values [StemNodeWidth][]byte
+ return values[:], nil
+
+ default:
+ return nil, fmt.Errorf("getValuesAtStem: unexpected node kind %d", cur.Kind())
+ }
+ }
+}
+
+// InsertSingle writes a single value slot at (stem, suffix). Thin wrapper
+// over InsertValuesAtStem — builds a stack-allocated 256-slot array with
+// only the target slot set and delegates. Matches the original design
+// gballet referenced (comment 3101751325): one primary insert path; the
+// single-slot variant dispatches through it so the split / resolve logic
+// lives in one place.
+func (s *nodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolver nodeResolverFn) error {
+ if len(value) != HashSize {
+ return errors.New("invalid insertion: value length")
+ }
+ var values [StemNodeWidth][]byte
+ values[suffix] = value
+ return s.InsertValuesAtStem(stem, values[:], resolver)
+}
+
+// InsertValuesAtStem writes the supplied value slots at stem. values may be
+// sparse (nil entries are ignored). The recursive implementation dispatches
+// through the same body, so a single code path handles internal descent,
+// HashedNode resolution, stem merge, and stem split.
+func (s *nodeStore) InsertValuesAtStem(stem []byte, values [][]byte, resolver nodeResolverFn) error {
+ var err error
+ s.root, err = s.insertValuesAtStem(s.root, stem, values, resolver, 0)
+ return err
+}
+
+func (s *nodeStore) insertValuesAtStem(ref nodeRef, stem []byte, values [][]byte, resolver nodeResolverFn, depth int) (nodeRef, error) {
+ switch ref.Kind() {
+ case kindInternal:
+ node := s.getInternal(ref.Index())
+ bit := stem[node.depth/8] >> (7 - (node.depth % 8)) & 1
+ if bit == 0 {
+ if node.left.Kind() == kindHashed {
+ if resolver == nil {
+ return ref, errors.New("insertValuesAtStem: cannot resolve hashed node without resolver")
+ }
+ hn := s.getHashed(node.left.Index())
+ path, err := keyToPath(int(node.depth), stem)
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem path error: %w", err)
+ }
+ data, err := resolver(path, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
+ }
+ resolved, err := s.deserializeNodeWithHash(data, int(node.depth)+1, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem deserialization error: %w", err)
+ }
+ s.freeHashedNode(node.left.Index())
+ node.left = resolved
+ }
+ newChild, err := s.insertValuesAtStem(node.left, stem, values, resolver, depth+1)
+ if err != nil {
+ return ref, err
+ }
+ node.left = newChild
+ } else {
+ if node.right.Kind() == kindHashed {
+ if resolver == nil {
+ return ref, errors.New("insertValuesAtStem: cannot resolve hashed node without resolver")
+ }
+ hn := s.getHashed(node.right.Index())
+ path, err := keyToPath(int(node.depth), stem)
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem path error: %w", err)
+ }
+ data, err := resolver(path, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
+ }
+ resolved, err := s.deserializeNodeWithHash(data, int(node.depth)+1, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem deserialization error: %w", err)
+ }
+ s.freeHashedNode(node.right.Index())
+ node.right = resolved
+ }
+ newChild, err := s.insertValuesAtStem(node.right, stem, values, resolver, depth+1)
+ if err != nil {
+ return ref, err
+ }
+ node.right = newChild
+ }
+ node.mustRecompute = true
+ node.dirty = true
+ return ref, nil
+
+ case kindStem:
+ sn := s.getStem(ref.Index())
+ if sn.Stem == [StemSize]byte(stem[:StemSize]) {
+ // Same stem — merge values (setValue marks dirty+mustRecompute)
+ for i, v := range values {
+ if v != nil {
+ sn.setValue(byte(i), v)
+ }
+ }
+ return ref, nil
+ }
+ // Different stem — split
+ return s.splitStemValuesInsert(ref, stem, values, resolver, depth)
+
+ case kindHashed:
+ hn := s.getHashed(ref.Index())
+ path, err := keyToPath(depth, stem)
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem path error: %w", err)
+ }
+ if resolver == nil {
+ return ref, errors.New("InsertValuesAtStem: resolver is nil")
+ }
+ data, err := resolver(path, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
+ }
+ resolved, err := s.deserializeNodeWithHash(data, depth, hn.Hash())
+ if err != nil {
+ return ref, fmt.Errorf("InsertValuesAtStem deserialization error: %w", err)
+ }
+ s.freeHashedNode(ref.Index())
+ return s.insertValuesAtStem(resolved, stem, values, resolver, depth)
+
+ case kindEmpty:
+ // Create new StemNode. Flag flips before the value loop so an
+ // all-nil values input still marks the newly-created stem dirty.
+ stemIdx := s.allocStem()
+ sn := s.getStem(stemIdx)
+ copy(sn.Stem[:], stem[:StemSize])
+ sn.depth = uint8(depth)
+ sn.mustRecompute = true
+ sn.dirty = true
+ for i, v := range values {
+ if v != nil {
+ sn.setValue(byte(i), v)
+ }
+ }
+ return makeRef(kindStem, stemIdx), nil
+
+ default:
+ return ref, fmt.Errorf("insertValuesAtStem: unexpected kind %d", ref.Kind())
+ }
+}
+
+// splitStemValuesInsert splits a StemNode when the new stem diverges.
+func (s *nodeStore) splitStemValuesInsert(existingRef nodeRef, newStem []byte, values [][]byte, resolver nodeResolverFn, depth int) (nodeRef, error) {
+ existing := s.getStem(existingRef.Index())
+
+ if int(existing.depth) >= StemSize*8 {
+ panic("splitStemValuesInsert: identical stems")
+ }
+
+ bitStem := existing.Stem[existing.depth/8] >> (7 - (existing.depth % 8)) & 1
+ nRef := s.newInternalRef(int(existing.depth))
+ nNode := s.getInternal(nRef.Index())
+ existing.depth++
+
+ bitKey := newStem[nNode.depth/8] >> (7 - (nNode.depth % 8)) & 1
+ if bitKey == bitStem {
+ // Same direction — need deeper split
+ var child nodeRef
+ if bitStem == 0 {
+ nNode.left = existingRef
+ child = nNode.left
+ } else {
+ nNode.right = existingRef
+ child = nNode.right
+ }
+ newChild, err := s.insertValuesAtStem(child, newStem, values, resolver, depth+1)
+ if err != nil {
+ // Roll back the depth increment so a retry sees the same
+ // existing state and extracts bitStem at the correct offset.
+ // nRef itself leaks (no internal free-list), but the slot is
+ // unreachable from the tree and harmless.
+ existing.depth--
+ return nRef, err
+ }
+ if bitStem == 0 {
+ nNode.left = newChild
+ nNode.right = emptyRef
+ } else {
+ nNode.right = newChild
+ nNode.left = emptyRef
+ }
+ } else {
+ // Divergence — create new StemNode for the new values
+ newStemIdx := s.allocStem()
+ newSn := s.getStem(newStemIdx)
+ copy(newSn.Stem[:], newStem[:StemSize])
+ newSn.depth = nNode.depth + 1
+ newSn.mustRecompute = true
+ newSn.dirty = true
+ for i, v := range values {
+ if v != nil {
+ newSn.setValue(byte(i), v)
+ }
+ }
+ newStemRef := makeRef(kindStem, newStemIdx)
+
+ if bitStem == 0 {
+ nNode.left = existingRef
+ nNode.right = newStemRef
+ } else {
+ nNode.left = newStemRef
+ nNode.right = existingRef
+ }
+ }
+ return nRef, nil
+}
+
+func (s *nodeStore) Insert(key []byte, value []byte, resolver nodeResolverFn) error {
+ return s.InsertSingle(key[:StemSize], key[StemSize], value, resolver)
+}
+
+func (s *nodeStore) Get(key []byte, resolver nodeResolverFn) ([]byte, error) {
+ return s.GetValue(key[:StemSize], key[StemSize], resolver)
+}
+
+func (s *nodeStore) getHeight(ref nodeRef) int {
+ switch ref.Kind() {
+ case kindInternal:
+ node := s.getInternal(ref.Index())
+ lh := s.getHeight(node.left)
+ rh := s.getHeight(node.right)
+ if lh > rh {
+ return 1 + lh
+ }
+ return 1 + rh
+ case kindStem:
+ return 1
+ case kindEmpty:
+ return 0
+ default:
+ return 0
+ }
+}
diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go
index b1e3c991c0..e3436e3df1 100644
--- a/trie/bintrie/trie.go
+++ b/trie/bintrie/trie.go
@@ -19,7 +19,6 @@ package bintrie
import (
"bytes"
"encoding/binary"
- "errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
@@ -31,8 +30,6 @@ import (
"github.com/holiman/uint256"
)
-var errInvalidRootType = errors.New("invalid root type")
-
// ChunkedCode represents a sequence of HashSize-byte chunks of code (StemSize bytes of which
// are actual code, and NodeTypeBytes byte is the pushdata offset).
type ChunkedCode []byte
@@ -108,34 +105,39 @@ func ChunkifyCode(code []byte) ChunkedCode {
return chunks
}
-// NewBinaryNode creates a new empty binary trie
-func NewBinaryNode() BinaryNode {
- return Empty{}
-}
-
// BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864.
type BinaryTrie struct {
- root BinaryNode
- 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.
func (t *BinaryTrie) ToDot() string {
- t.root.Hash()
- return ToDot(t.root)
+ t.store.computeHash(t.store.root)
+ return t.store.toDot(t.store.root, "", "")
}
// 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{
- root: NewBinaryNode(),
- 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 {
@@ -143,11 +145,11 @@ func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, err
if err != nil {
return nil, err
}
- node, err := DeserializeNodeWithHash(blob, 0, root)
+ ref, err := t.store.deserializeNodeWithHash(blob, 0, root)
if err != nil {
return nil, err
}
- t.root = node
+ t.store.root = ref
}
return t, nil
}
@@ -176,29 +178,18 @@ func (t *BinaryTrie) GetKey(key []byte) []byte {
// GetWithHashedKey returns the value, assuming that the key has already
// been hashed.
func (t *BinaryTrie) GetWithHashedKey(key []byte) ([]byte, error) {
- return t.root.Get(key, t.nodeResolver)
+ return t.store.Get(key, t.nodeResolver)
}
// GetAccount returns the account information for the given address.
func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
var (
- values [][]byte
- err error
- acc = &types.StateAccount{}
- key = GetBinaryTreeKey(addr, zero[:])
+ err error
+ acc = &types.StateAccount{}
+ key = GetBinaryTreeKey(addr, zero[:])
)
- switch r := t.root.(type) {
- case *InternalNode:
- values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver)
- case *StemNode:
- values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver)
- case Empty:
- return nil, nil
- default:
- // This will cover HashedNode but that should be fine since the
- // root node should always be resolved.
- return nil, errInvalidRootType
- }
+
+ values, err := t.store.GetValuesAtStem(key[:StemSize], t.nodeResolver)
if err != nil {
return nil, fmt.Errorf("GetAccount (%x) error: %v", addr, err)
}
@@ -219,7 +210,7 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
// If the account has been deleted, BasicData and CodeHash will both be
// 32-byte zero blobs (not nil). If the account is recreated afterwards,
// UpdateAccount overwrites BasicData and CodeHash with non-zero values,
- // so this branch won't activate..
+ // so this branch won't activate.
if bytes.Equal(values[BasicDataLeafKey], zero[:]) &&
bytes.Equal(values[CodeHashLeafKey], zero[:]) {
return nil, nil
@@ -238,13 +229,12 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
// not be modified by the caller. If a node was not found in the database, a
// trie.MissingNodeError is returned.
func (t *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
- return t.root.Get(GetBinaryTreeKeyStorageSlot(addr, key), t.nodeResolver)
+ return t.store.Get(GetBinaryTreeKeyStorageSlot(addr, key), t.nodeResolver)
}
// UpdateAccount updates the account information for the given address.
func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
var (
- err error
basicData [HashSize]byte
values = make([][]byte, StemNodeWidth)
stem = GetBinaryTreeKey(addr, zero[:])
@@ -265,15 +255,12 @@ func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount,
values[BasicDataLeafKey] = basicData[:]
values[CodeHashLeafKey] = acc.CodeHash[:]
- t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
- return err
+ return t.store.InsertValuesAtStem(stem, values, t.nodeResolver)
}
// UpdateStem updates the values for the given stem key.
func (t *BinaryTrie) UpdateStem(key []byte, values [][]byte) error {
- var err error
- t.root, err = t.root.InsertValuesAtStem(key, values, t.nodeResolver, 0)
- return err
+ return t.store.InsertValuesAtStem(key, values, t.nodeResolver)
}
// UpdateStorage associates key with value in the trie. If value has length zero, any
@@ -288,11 +275,10 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er
} else {
copy(v[HashSize-len(value):], value[:])
}
- root, err := t.root.Insert(k, v[:], t.nodeResolver, 0)
+ err := t.store.Insert(k, v[:], t.nodeResolver)
if err != nil {
return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
}
- t.root = root
return nil
}
@@ -307,12 +293,7 @@ func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
values[BasicDataLeafKey] = zero[:]
values[CodeHashLeafKey] = zero[:]
- root, err := t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
- if err != nil {
- return fmt.Errorf("DeleteAccount (%x) error: %v", addr, err)
- }
- t.root = root
- return nil
+ return t.store.InsertValuesAtStem(stem, values, t.nodeResolver)
}
// DeleteStorage removes any existing value for key from the trie. If a node was not
@@ -320,18 +301,17 @@ func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
k := GetBinaryTreeKeyStorageSlot(addr, key)
var zero [HashSize]byte
- root, err := t.root.Insert(k, zero[:], t.nodeResolver, 0)
+ err := t.store.Insert(k, zero[:], t.nodeResolver)
if err != nil {
return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
}
- t.root = root
return nil
}
// Hash returns the root hash of the trie. It does not write to the database and
// can be used even if the trie doesn't have one.
func (t *BinaryTrie) Hash() common.Hash {
- return t.root.Hash()
+ return t.store.computeHash(t.store.root)
}
// Commit writes all nodes to the trie's memory database, tracking the internal
@@ -339,15 +319,12 @@ func (t *BinaryTrie) Hash() common.Hash {
func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{})
- // The root can be any type of BinaryNode (InternalNode, StemNode, etc.)
- err := t.root.CollectNodes(nil, func(path []byte, node BinaryNode) {
- serialized := SerializeNode(node)
- nodeset.AddNode(path, trienode.NewNodeWithPrev(node.Hash(), serialized, t.tracer.Get(path)))
- })
- if err != nil {
- panic(fmt.Errorf("CollectNodes failed: %v", err))
- }
- // Serialize root commitment form
+ // 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)
+ 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)))
+ }, t.groupDepth)
return t.Hash(), nodeset
}
@@ -371,14 +348,15 @@ 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{
- root: t.root.Copy(),
- reader: t.reader,
- tracer: t.tracer.Copy(),
+ store: t.store.Copy(),
+ reader: t.reader,
+ tracer: t.tracer.Copy(),
+ groupDepth: t.groupDepth,
}
}
-// IsVerkle returns true if the trie is a Verkle tree.
-func (t *BinaryTrie) IsVerkle() bool {
+// IsUBT returns true if the trie is a Verkle tree.
+func (t *BinaryTrie) IsUBT() bool {
// TODO @gballet This is technically NOT a verkle tree, but it has the same
// behavior and basic structure, so for all intents and purposes, it can be
// treated as such. Rename this when verkle gets removed.
@@ -407,7 +385,6 @@ func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Has
if groupOffset == StemNodeWidth-1 || len(chunks)-i <= HashSize {
err = t.UpdateStem(key[:StemSize], values)
-
if err != nil {
return fmt.Errorf("UpdateContractCode (addr=%x) error: %w", addr[:], err)
}
diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go
index 5b104ddde4..8b7d9e46d6 100644
--- a/trie/bintrie/trie_test.go
+++ b/trie/bintrie/trie_test.go
@@ -37,147 +37,130 @@ var (
)
func TestSingleEntry(t *testing.T) {
- tree := NewBinaryNode()
- tree, err := tree.Insert(zeroKey[:], oneKey[:], nil, 0)
- if err != nil {
+ s := newNodeStore()
+ if err := s.Insert(zeroKey[:], oneKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 1 {
+ if s.getHeight(s.root) != 1 {
t.Fatal("invalid depth")
}
expected := common.HexToHash("aab1060e04cb4f5dc6f697ae93156a95714debbf77d54238766adc5709282b6f")
- got := tree.Hash()
+ got := s.Hash()
if got != expected {
t.Fatalf("invalid tree root, got %x, want %x", got, expected)
}
}
func TestTwoEntriesDiffFirstBit(t *testing.T) {
- var err error
- tree := NewBinaryNode()
- tree, err = tree.Insert(zeroKey[:], oneKey[:], nil, 0)
- if err != nil {
+ s := newNodeStore()
+ if err := s.Insert(zeroKey[:], oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 2 {
+ if s.getHeight(s.root) != 2 {
t.Fatal("invalid height")
}
- if tree.Hash() != common.HexToHash("dfc69c94013a8b3c65395625a719a87534a7cfd38719251ad8c8ea7fe79f065e") {
+ if s.Hash() != common.HexToHash("dfc69c94013a8b3c65395625a719a87534a7cfd38719251ad8c8ea7fe79f065e") {
t.Fatal("invalid tree root")
}
}
func TestOneStemColocatedValues(t *testing.T) {
- var err error
- tree := NewBinaryNode()
- tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
- if err != nil {
+ s := newNodeStore()
+ if err := s.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 1 {
+ if s.getHeight(s.root) != 1 {
t.Fatal("invalid height")
}
}
func TestTwoStemColocatedValues(t *testing.T) {
- var err error
- tree := NewBinaryNode()
+ s := newNodeStore()
// stem: 0...0
- tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil); err != nil {
t.Fatal(err)
}
// stem: 10...0
- tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 2 {
+ if s.getHeight(s.root) != 2 {
t.Fatal("invalid height")
}
}
func TestTwoKeysMatchFirst42Bits(t *testing.T) {
- var err error
- tree := NewBinaryNode()
+ s := newNodeStore()
// key1 and key 2 have the same prefix of 42 bits (b0*42+b1+b1) and differ after.
key1 := common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0").Bytes()
key2 := common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000").Bytes()
- tree, err = tree.Insert(key1, oneKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(key1, oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(key2, twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(key2, twoKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 1+42+1 {
+ if s.getHeight(s.root) != 1+42+1 {
t.Fatal("invalid height")
}
}
+
func TestInsertDuplicateKey(t *testing.T) {
- var err error
- tree := NewBinaryNode()
- tree, err = tree.Insert(oneKey[:], oneKey[:], nil, 0)
- if err != nil {
+ s := newNodeStore()
+ if err := s.Insert(oneKey[:], oneKey[:], nil); err != nil {
t.Fatal(err)
}
- tree, err = tree.Insert(oneKey[:], twoKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(oneKey[:], twoKey[:], nil); err != nil {
t.Fatal(err)
}
- if tree.GetHeight() != 1 {
+ if s.getHeight(s.root) != 1 {
t.Fatal("invalid height")
}
// Verify that the value is updated
- if !bytes.Equal(tree.(*StemNode).Values[1], twoKey[:]) {
- t.Fatal("invalid height")
+ v, err := s.Get(oneKey[:], nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(v, twoKey[:]) {
+ t.Fatal("value not updated")
}
}
+
func TestLargeNumberOfEntries(t *testing.T) {
- var err error
- tree := NewBinaryNode()
+ s := newNodeStore()
for i := range StemNodeWidth {
var key [HashSize]byte
key[0] = byte(i)
- tree, err = tree.Insert(key[:], ffKey[:], nil, 0)
- if err != nil {
+ if err := s.Insert(key[:], ffKey[:], nil); err != nil {
t.Fatal(err)
}
}
- height := tree.GetHeight()
+ height := s.getHeight(s.root)
if height != 1+8 {
t.Fatalf("invalid height, wanted %d, got %d", 1+8, height)
}
}
func TestMerkleizeMultipleEntries(t *testing.T) {
- var err error
- tree := NewBinaryNode()
+ s := newNodeStore()
keys := [][]byte{
zeroKey[:],
common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(),
@@ -187,12 +170,11 @@ func TestMerkleizeMultipleEntries(t *testing.T) {
for i, key := range keys {
var v [HashSize]byte
binary.LittleEndian.PutUint64(v[:8], uint64(i))
- tree, err = tree.Insert(key, v[:], nil, 0)
- if err != nil {
+ if err := s.Insert(key, v[:], nil); err != nil {
t.Fatal(err)
}
}
- got := tree.Hash()
+ got := s.Hash()
expected := common.HexToHash("9317155862f7a3867660ddd0966ff799a3d16aa4df1e70a7516eaa4a675191b5")
if got != expected {
t.Fatalf("invalid root, expected=%x, got = %x", expected, got)
@@ -206,7 +188,7 @@ func TestMerkleizeMultipleEntries(t *testing.T) {
func TestStorageRoundTrip(t *testing.T) {
tracer := trie.NewPrevalueTracer()
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: tracer,
}
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
@@ -274,7 +256,7 @@ func TestStorageRoundTrip(t *testing.T) {
func newEmptyTestTrie(t *testing.T) *BinaryTrie {
t.Helper()
return &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
}
}
@@ -599,7 +581,7 @@ func TestBinaryTrieWitness(t *testing.T) {
tracer := trie.NewPrevalueTracer()
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: tracer,
}
if w := tr.Witness(); len(w) != 0 {
@@ -626,7 +608,7 @@ func TestBinaryTrieWitness(t *testing.T) {
func testAccount(t *testing.T, addr common.Address, nonce uint64, balance uint64) *BinaryTrie {
t.Helper()
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
}
acc := &types.StateAccount{
@@ -649,8 +631,8 @@ func TestGetAccountNonMembershipStemRoot(t *testing.T) {
tr := testAccount(t, addr, 42, 100)
// Verify root is a StemNode (single stem inserted).
- if _, ok := tr.root.(*StemNode); !ok {
- t.Fatalf("expected StemNode root, got %T", tr.root)
+ if tr.store.root.Kind() != kindStem {
+ t.Fatalf("expected StemNode root, got kind %d", tr.store.root.Kind())
}
// Query a completely different address — must return nil.
@@ -680,7 +662,7 @@ func TestGetAccountNonMembershipStemRoot(t *testing.T) {
// address returns nil when the trie root is an InternalNode (multi-account trie).
func TestGetAccountNonMembershipInternalRoot(t *testing.T) {
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
}
@@ -700,8 +682,8 @@ func TestGetAccountNonMembershipInternalRoot(t *testing.T) {
}
// Verify root is an InternalNode.
- if _, ok := tr.root.(*InternalNode); !ok {
- t.Fatalf("expected InternalNode root, got %T", tr.root)
+ if tr.store.root.Kind() != kindInternal {
+ t.Fatalf("expected InternalNode root, got kind %d", tr.store.root.Kind())
}
// Query a non-existent address — must return nil.
@@ -723,8 +705,8 @@ func TestGetStorageNonMembershipStemRoot(t *testing.T) {
tr := testAccount(t, addr, 1, 100)
// Verify root is a StemNode.
- if _, ok := tr.root.(*StemNode); !ok {
- t.Fatalf("expected StemNode root, got %T", tr.root)
+ if tr.store.root.Kind() != kindStem {
+ t.Fatalf("expected StemNode root, got kind %d", tr.store.root.Kind())
}
// Query storage for a different address — must return nil, not panic.
@@ -743,7 +725,7 @@ func TestGetStorageNonMembershipStemRoot(t *testing.T) {
// non-existent address returns nil when the root is an InternalNode.
func TestGetStorageNonMembershipInternalRoot(t *testing.T) {
tr := &BinaryTrie{
- root: NewBinaryNode(),
+ store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
}
@@ -765,8 +747,8 @@ func TestGetStorageNonMembershipInternalRoot(t *testing.T) {
t.Fatalf("UpdateStorage error: %v", err)
}
- if _, ok := tr.root.(*InternalNode); !ok {
- t.Fatalf("expected InternalNode root, got %T", tr.root)
+ if tr.store.root.Kind() != kindInternal {
+ t.Fatalf("expected InternalNode root, got kind %d", tr.store.root.Kind())
}
// Query storage for a non-existent address — must return nil.
@@ -779,3 +761,101 @@ func TestGetStorageNonMembershipInternalRoot(t *testing.T) {
t.Fatalf("expected nil/zero for non-existent storage, got %x", got)
}
}
+
+// TestCommitSkipCleanSubtrees verifies that CollectNodes short-circuits on
+// clean subtrees. First Commit flushes every resolved node; a follow-up
+// Commit with no modifications flushes nothing; a single-leaf modification
+// flushes only the root-to-leaf path.
+func TestCommitSkipCleanSubtrees(t *testing.T) {
+ tr := &BinaryTrie{
+ store: newNodeStore(),
+ tracer: trie.NewPrevalueTracer(),
+ groupDepth: 1,
+ }
+ const n = 200
+ key := func(i int) [HashSize]byte {
+ var k [HashSize]byte
+ binary.BigEndian.PutUint64(k[:8], uint64(i+1)*0x9e3779b97f4a7c15)
+ binary.BigEndian.PutUint64(k[8:16], uint64(i+1)*0xc2b2ae3d27d4eb4f)
+ binary.BigEndian.PutUint64(k[16:24], uint64(i+1)*0x165667b19e3779f9)
+ binary.BigEndian.PutUint64(k[24:32], uint64(i+1)*0x85ebca77c2b2ae63)
+ return k
+ }
+ for i := range n {
+ k := key(i)
+ var v [HashSize]byte
+ binary.BigEndian.PutUint64(v[24:], uint64(i+1))
+ if err := tr.store.Insert(k[:], v[:], nil); err != nil {
+ t.Fatalf("Insert %d: %v", i, err)
+ }
+ }
+
+ _, ns1 := tr.Commit(false)
+ if len(ns1.Nodes) == 0 {
+ t.Fatal("first Commit produced empty NodeSet")
+ }
+
+ _, nsNoop := tr.Commit(false)
+ if len(nsNoop.Nodes) != 0 {
+ t.Fatalf("no-op Commit: expected empty NodeSet, got %d", len(nsNoop.Nodes))
+ }
+
+ // Modify a single leaf — only the root-to-leaf path should flush.
+ k := key(n / 2)
+ var newVal [HashSize]byte
+ newVal[0] = 0xff
+ if err := tr.store.Insert(k[:], newVal[:], nil); err != nil {
+ t.Fatalf("Insert (modify): %v", err)
+ }
+ _, ns2 := tr.Commit(false)
+ if len(ns2.Nodes) == 0 {
+ t.Fatal("modified Commit produced empty NodeSet")
+ }
+ if len(ns2.Nodes) > 32 {
+ t.Fatalf("modified Commit: expected ≤32 nodes (path+stem), got %d", len(ns2.Nodes))
+ }
+ if len(ns2.Nodes) >= len(ns1.Nodes) {
+ t.Fatalf("expected second NodeSet (%d) to be smaller than first (%d)", len(ns2.Nodes), len(ns1.Nodes))
+ }
+}
+
+// BenchmarkCollectNodesSparseWrite measures Commit cost when one leaf
+// changes per block — the common case for state updates. After warm-up
+// (populate + initial Commit), each iteration modifies a single leaf and
+// re-Commits. Matches the shape of the same-named benchmark on master so
+// the two trees can be benchstat'd directly.
+func BenchmarkCollectNodesSparseWrite(b *testing.B) {
+ const n = 10_000
+ tr := &BinaryTrie{
+ store: newNodeStore(),
+ tracer: trie.NewPrevalueTracer(),
+ }
+ keys := make([][HashSize]byte, n)
+ for i := range n {
+ binary.BigEndian.PutUint64(keys[i][:8], uint64(i+1)*0x9e3779b97f4a7c15)
+ binary.BigEndian.PutUint64(keys[i][8:16], uint64(i+1)*0xc2b2ae3d27d4eb4f)
+ binary.BigEndian.PutUint64(keys[i][16:24], uint64(i+1)*0x165667b19e3779f9)
+ binary.BigEndian.PutUint64(keys[i][24:32], uint64(i+1)*0x85ebca77c2b2ae63)
+ var v [HashSize]byte
+ binary.BigEndian.PutUint64(v[24:], uint64(i+1))
+ if err := tr.store.Insert(keys[i][:], v[:], nil); err != nil {
+ b.Fatalf("warmup Insert %d: %v", i, err)
+ }
+ }
+ _, _ = tr.Commit(false) // warmup flush
+
+ var newVal [HashSize]byte
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ idx := i % n
+ binary.BigEndian.PutUint64(newVal[24:], uint64(i+1))
+ if err := tr.store.Insert(keys[idx][:], newVal[:], nil); err != nil {
+ b.Fatalf("iter %d Insert: %v", i, err)
+ }
+ _, ns := tr.Commit(false)
+ if len(ns.Nodes) == 0 {
+ b.Fatalf("iter %d: empty NodeSet", i)
+ }
+ }
+}
diff --git a/trie/secure_trie.go b/trie/secure_trie.go
index 1f150ede8c..4d03ca45f0 100644
--- a/trie/secure_trie.go
+++ b/trie/secure_trie.go
@@ -324,6 +324,6 @@ func (t *StateTrie) MustNodeIterator(start []byte) NodeIterator {
return t.trie.MustNodeIterator(start)
}
-func (t *StateTrie) IsVerkle() bool {
+func (t *StateTrie) IsUBT() bool {
return false
}
diff --git a/trie/transitiontrie/transition.go b/trie/transitiontrie/transition.go
index 4c73022082..3e5511be9e 100644
--- a/trie/transitiontrie/transition.go
+++ b/trie/transitiontrie/transition.go
@@ -202,8 +202,8 @@ func (t *TransitionTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
panic("not implemented") // TODO: Implement
}
-// IsVerkle returns true if the trie is verkle-tree based
-func (t *TransitionTrie) IsVerkle() bool {
+// IsUBT returns true if the trie is verkle-tree based
+func (t *TransitionTrie) IsUBT() bool {
// For all intents and purposes, the calling code should treat this as a verkle trie
return true
}
diff --git a/triedb/database.go b/triedb/database.go
index c1abe93462..ef95169df1 100644
--- a/triedb/database.go
+++ b/triedb/database.go
@@ -31,26 +31,30 @@ import (
// Config defines all necessary options for database.
type Config struct {
- Preimages bool // Flag whether the preimage of node key is recorded
- IsVerkle 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{
Preimages: false,
- IsVerkle: false,
+ IsUBT: false,
HashDB: hashdb.Defaults,
}
-// VerkleDefaults 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 VerkleDefaults = &Config{
- Preimages: false,
- IsVerkle: true,
- PathDB: pathdb.Defaults,
+var UBTDefaults = &Config{
+ Preimages: false,
+ IsUBT: true,
+ BinTrieGroupDepth: DefaultBinTrieGroupDepth,
+ PathDB: pathdb.Defaults,
}
// backend defines the methods needed to access/update trie nodes in different
@@ -109,7 +113,7 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database {
log.Crit("Both 'hash' and 'path' mode are configured")
}
if config.PathDB != nil {
- db.backend = pathdb.New(diskdb, config.PathDB, config.IsVerkle)
+ db.backend = pathdb.New(diskdb, config.PathDB, config.IsUBT)
} else {
db.backend = hashdb.New(diskdb, config.HashDB)
}
@@ -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
@@ -375,9 +389,9 @@ func (db *Database) IndexProgress() (uint64, uint64, error) {
return pdb.IndexProgress()
}
-// IsVerkle returns the indicator if the database is holding a verkle tree.
-func (db *Database) IsVerkle() bool {
- return db.config.IsVerkle
+// IsUBT returns the indicator if the database is holding a verkle tree.
+func (db *Database) IsUBT() bool {
+ return db.config.IsUBT
}
// Disk returns the underlying disk database.
@@ -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 a61d302b1d..e52949c93e 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -100,13 +100,9 @@ func merkleNodeHasher(blob []byte) (common.Hash, error) {
// binaryNodeHasher computes the hash of the given verkle node.
func binaryNodeHasher(blob []byte) (common.Hash, error) {
if len(blob) == 0 {
- return types.EmptyVerkleHash, nil
+ return types.EmptyBinaryHash, nil
}
- n, err := bintrie.DeserializeNode(blob, 0)
- if err != nil {
- return common.Hash{}, err
- }
- return n.Hash(), nil
+ return bintrie.DeserializeAndHash(blob, 0)
}
// Database is a multiple-layered structure for maintaining in-memory states
@@ -127,7 +123,7 @@ type Database struct {
// the shutdown to reject all following unexpected mutations.
readOnly bool // Flag if database is opened in read only mode
waitSync bool // Flag if database is deactivated due to initial state sync
- isVerkle bool // Flag if database is used for verkle tree
+ isUBT bool // Flag if database is used for verkle tree
hasher nodeHasher // Trie node hasher
config *Config // Configuration for database
@@ -146,7 +142,7 @@ type Database struct {
// New attempts to load an already existing layer from a persistent key-value
// store (with a number of memory layers from a journal). If the journal is not
// matched with the base persistent layer, all the recorded diff layers are discarded.
-func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
+func New(diskdb ethdb.Database, config *Config, isUBT bool) *Database {
if config == nil {
config = Defaults
}
@@ -154,7 +150,7 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
db := &Database{
readOnly: config.ReadOnly,
- isVerkle: isVerkle,
+ isUBT: isUBT,
config: config,
diskdb: diskdb,
hasher: merkleNodeHasher,
@@ -164,7 +160,7 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
// important to note that the introduction of a prefix won't lead to
// substantial storage overhead, as the underlying database will efficiently
// compress the shared key prefix.
- if isVerkle {
+ if isUBT {
db.diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix))
db.hasher = binaryNodeHasher
}
@@ -174,7 +170,7 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
// Repair the history, which might not be aligned with the persistent
// state in the key-value store due to an unclean shutdown.
- states, trienodes, err := repairHistory(db.diskdb, isVerkle, db.config.ReadOnly, db.tree.bottom().stateID(), db.config.TrienodeHistory >= 0)
+ states, trienodes, err := repairHistory(db.diskdb, isUBT, db.config.ReadOnly, db.tree.bottom().stateID(), db.config.TrienodeHistory >= 0)
if err != nil {
log.Crit("Failed to repair history", "err", err)
}
@@ -196,7 +192,7 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
db.setHistoryIndexer()
fields := config.fields()
- if db.isVerkle {
+ if db.isUBT {
fields = append(fields, "verkle", true)
}
log.Info("Initialized path database", fields...)
@@ -265,7 +261,7 @@ func (db *Database) setStateGenerator() error {
// - the database is opened in read only mode
// - the snapshot build is explicitly disabled
// - the database is opened in verkle tree mode
- noBuild := db.readOnly || db.config.SnapshotNoBuild || db.isVerkle
+ noBuild := db.readOnly || db.config.SnapshotNoBuild || db.isUBT
// Construct the generator and link it to the disk layer, ensuring that the
// generation progress is resolved to prevent accessing uncovered states
@@ -369,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 {
@@ -387,28 +376,41 @@ 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.
- db.tree.init(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild))
+// 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.
// To ensure the history indexer always matches the current state, we must:
@@ -420,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.
@@ -586,7 +625,7 @@ func (db *Database) journalPath() string {
return ""
}
var fname string
- if db.isVerkle {
+ if db.isUBT {
fname = fmt.Sprintf("verkle.journal")
} else {
fname = fmt.Sprintf("merkle.journal")
diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go
index e70a3ec2a2..41212dc9d0 100644
--- a/triedb/pathdb/database_test.go
+++ b/triedb/pathdb/database_test.go
@@ -143,7 +143,7 @@ type testerConfig struct {
layers int // Number of state transitions to generate for
enableIndex bool // Enable state history indexing or not
journalDir string // Directory path for persisting journal files
- isVerkle bool // Enables Verkle trie mode if true
+ isUBT bool // Enables Verkle trie mode if true
writeBuffer *int // Optional, the size of memory allocated for write buffer
trieCache *int // Optional, the size of memory allocated for trie cache
@@ -183,7 +183,7 @@ func newTester(t *testing.T, config *testerConfig) *tester {
NoAsyncFlush: true,
JournalDirectory: config.journalDir,
NoHistoryIndexDelay: true,
- }, config.isVerkle)
+ }, config.isUBT)
obj = &tester{
db: db,
@@ -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/history.go b/triedb/pathdb/history.go
index 0a9f7091fa..7f5b0e35ba 100644
--- a/triedb/pathdb/history.go
+++ b/triedb/pathdb/history.go
@@ -376,7 +376,7 @@ func syncHistory(stores ...ethdb.AncientWriter) error {
// persistent state may appear if the trienode history was disabled during the
// previous run. This process detects and resolves such gaps, preventing
// unexpected panics.
-func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint64, enableTrienode bool) (ethdb.ResettableAncientStore, ethdb.ResettableAncientStore, error) {
+func repairHistory(db ethdb.Database, isUBT bool, readOnly bool, stateID uint64, enableTrienode bool) (ethdb.ResettableAncientStore, ethdb.ResettableAncientStore, error) {
ancient, err := db.AncientDatadir()
if err != nil {
// TODO error out if ancient store is disabled. A tons of unit tests
@@ -386,7 +386,7 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint
}
// State history is mandatory as it is the key component that ensures
// resilience to deep reorgs.
- states, err := rawdb.NewStateFreezer(ancient, isVerkle, readOnly)
+ states, err := rawdb.NewStateFreezer(ancient, isUBT, readOnly)
if err != nil {
log.Crit("Failed to open state history freezer", "err", err)
}
@@ -395,7 +395,7 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint
// node with state proofs.
var trienodes ethdb.ResettableAncientStore
if enableTrienode {
- trienodes, err = rawdb.NewTrienodeFreezer(ancient, isVerkle, readOnly)
+ trienodes, err = rawdb.NewTrienodeFreezer(ancient, isUBT, readOnly)
if err != nil {
log.Crit("Failed to open trienode history freezer", "err", err)
}
diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go
index adb534f47d..191c2fadf5 100644
--- a/triedb/pathdb/iterator_test.go
+++ b/triedb/pathdb/iterator_test.go
@@ -369,7 +369,7 @@ func TestAccountIteratorTraversalValues(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 {
@@ -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/triedb/pathdb/layertree_test.go b/triedb/pathdb/layertree_test.go
index 82eb182990..0dcfd7aae8 100644
--- a/triedb/pathdb/layertree_test.go
+++ b/triedb/pathdb/layertree_test.go
@@ -55,9 +55,9 @@ func TestLayerCap(t *testing.T) {
layers: 2,
base: common.Hash{0x2},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x2}: {},
- common.Hash{0x3}: {},
- common.Hash{0x4}: {},
+ {0x2}: {},
+ {0x3}: {},
+ {0x4}: {},
},
},
{
@@ -76,8 +76,8 @@ func TestLayerCap(t *testing.T) {
layers: 1,
base: common.Hash{0x3},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x3}: {},
- common.Hash{0x4}: {},
+ {0x3}: {},
+ {0x4}: {},
},
},
{
@@ -96,7 +96,7 @@ func TestLayerCap(t *testing.T) {
layers: 0,
base: common.Hash{0x4},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x4}: {},
+ {0x4}: {},
},
},
{
@@ -119,9 +119,9 @@ func TestLayerCap(t *testing.T) {
layers: 2,
base: common.Hash{0x2a},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x4a}: {},
- common.Hash{0x3a}: {},
- common.Hash{0x2a}: {},
+ {0x4a}: {},
+ {0x3a}: {},
+ {0x2a}: {},
},
},
{
@@ -144,8 +144,8 @@ func TestLayerCap(t *testing.T) {
layers: 1,
base: common.Hash{0x3a},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x4a}: {},
- common.Hash{0x3a}: {},
+ {0x4a}: {},
+ {0x3a}: {},
},
},
{
@@ -168,11 +168,11 @@ func TestLayerCap(t *testing.T) {
layers: 2,
base: common.Hash{0x2},
snapshot: map[common.Hash]struct{}{
- common.Hash{0x4a}: {},
- common.Hash{0x3a}: {},
- common.Hash{0x4b}: {},
- common.Hash{0x3b}: {},
- common.Hash{0x2}: {},
+ {0x4a}: {},
+ {0x3a}: {},
+ {0x4b}: {},
+ {0x3b}: {},
+ {0x2}: {},
},
},
}
@@ -261,7 +261,7 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
},
},
@@ -271,11 +271,11 @@ func TestDescendant(t *testing.T) {
tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
common.Hash{0x3}: {},
},
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3}: {},
},
},
@@ -291,16 +291,16 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x3}: {
+ {0x3}: {
common.Hash{0x4}: {},
},
},
@@ -310,11 +310,11 @@ func TestDescendant(t *testing.T) {
tr.cap(common.Hash{0x4}, 2)
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x3}: {
+ {0x3}: {
common.Hash{0x4}: {},
},
},
@@ -330,16 +330,16 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x3}: {
+ {0x3}: {
common.Hash{0x4}: {},
},
},
@@ -349,7 +349,7 @@ func TestDescendant(t *testing.T) {
tr.cap(common.Hash{0x4}, 1)
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x3}: {
+ {0x3}: {
common.Hash{0x4}: {},
},
},
@@ -365,16 +365,16 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3}: {},
common.Hash{0x4}: {},
},
- common.Hash{0x3}: {
+ {0x3}: {
common.Hash{0x4}: {},
},
},
@@ -400,7 +400,7 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2a}: {},
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
@@ -408,18 +408,18 @@ func TestDescendant(t *testing.T) {
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x2a}: {
+ {0x2a}: {
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
},
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
- common.Hash{0x2b}: {
+ {0x2b}: {
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x3b}: {
+ {0x3b}: {
common.Hash{0x4b}: {},
},
},
@@ -429,11 +429,11 @@ func TestDescendant(t *testing.T) {
tr.cap(common.Hash{0x4a}, 2)
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x2a}: {
+ {0x2a}: {
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
},
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
},
@@ -453,7 +453,7 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2a}: {},
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
@@ -461,18 +461,18 @@ func TestDescendant(t *testing.T) {
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x2a}: {
+ {0x2a}: {
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
},
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
- common.Hash{0x2b}: {
+ {0x2b}: {
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x3b}: {
+ {0x3b}: {
common.Hash{0x4b}: {},
},
},
@@ -482,7 +482,7 @@ func TestDescendant(t *testing.T) {
tr.cap(common.Hash{0x4a}, 1)
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
},
@@ -501,23 +501,23 @@ func TestDescendant(t *testing.T) {
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x1}: {
+ {0x1}: {
common.Hash{0x2}: {},
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
- common.Hash{0x3b}: {
+ {0x3b}: {
common.Hash{0x4b}: {},
},
},
@@ -528,16 +528,16 @@ func TestDescendant(t *testing.T) {
tr.cap(common.Hash{0x4a}, 2)
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
- common.Hash{0x2}: {
+ {0x2}: {
common.Hash{0x3a}: {},
common.Hash{0x4a}: {},
common.Hash{0x3b}: {},
common.Hash{0x4b}: {},
},
- common.Hash{0x3a}: {
+ {0x3a}: {
common.Hash{0x4a}: {},
},
- common.Hash{0x3b}: {
+ {0x3b}: {
common.Hash{0x4b}: {},
},
},
diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go
index e3cfbcba8a..e087ef26ed 100644
--- a/triedb/pathdb/reader.go
+++ b/triedb/pathdb/reader.go
@@ -177,7 +177,7 @@ func (db *Database) NodeReader(root common.Hash) (database.NodeReader, error) {
return &reader{
db: db,
state: root,
- noHashCheck: db.isVerkle,
+ noHashCheck: db.isUBT,
layer: layer,
}, nil
}
@@ -229,7 +229,7 @@ func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, er
return nil, err // e.g., the referred state history has been pruned
}
if meta.parent != root {
- return nil, fmt.Errorf("state %#x is not canonincal", root)
+ return nil, fmt.Errorf("state %#x is not canonical", root)
}
return &HistoricalStateReader{
id: *id,
@@ -352,7 +352,7 @@ func (db *Database) HistoricNodeReader(root common.Hash) (*HistoricalNodeReader,
return nil, fmt.Errorf("state %#x is not available", root) // e.g., the referred trienode history has been pruned
}
if meta.parent != root {
- return nil, fmt.Errorf("state %#x is not canonincal", root)
+ return nil, fmt.Errorf("state %#x is not canonical", root)
}
return &HistoricalNodeReader{
id: *id,
diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go
index c54d8b1136..27a6c1d422 100644
--- a/triedb/pathdb/states.go
+++ b/triedb/pathdb/states.go
@@ -583,6 +583,18 @@ func (s *StateSetWithOrigin) decode(r *rlp.Stream) error {
}
}
s.storageOrigin = storageSet
+
+ // Compute the size of origin data, keeping consistent with NewStateSetWithOrigin
+ var size int
+ for _, data := range s.accountOrigin {
+ size += common.HashLength + len(data)
+ }
+ for _, slots := range s.storageOrigin {
+ for _, data := range slots {
+ size += 2*common.HashLength + len(data)
+ }
+ }
+ s.size = s.stateSet.size + uint64(size)
return nil
}
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
)