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 )