diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index efe76cf170..a3fa4a2ea7 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -145,7 +145,7 @@ jobs: windows: name: Windows Build - runs-on: "win-11" + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -155,57 +155,46 @@ jobs: go-version: 1.24 cache: false - # Note: gcc.exe only works properly if the corresponding bin/ directory is - # contained in PATH. + - name: Install cross toolchain + run: | + apt-get update + apt-get -yq --no-install-suggests --no-install-recommends install \ + gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 nsis - name: "Build (amd64)" - shell: cmd run: | - set PATH=%GETH_MINGW%\bin;%PATH% - go run build/ci.go install -dlgo -arch amd64 -cc %GETH_MINGW%\bin\gcc.exe - env: - GETH_MINGW: 'C:\msys64\mingw64' + go run build/ci.go install -dlgo -os windows -arch amd64 -cc x86_64-w64-mingw32-gcc - name: "Create/upload archive (amd64)" - shell: cmd run: | - go run build/ci.go archive -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds + go run build/ci.go archive -os windows -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Create/upload NSIS installer (amd64)" - shell: cmd run: | - set "PATH=C:\Program Files (x86)\NSIS;%PATH%" go run build/ci.go nsis -arch amd64 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - del /Q build\bin\* + rm -f build/bin/* env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Build (386)" - shell: cmd run: | - set PATH=%GETH_MINGW%\bin;%PATH% - go run build/ci.go install -dlgo -arch 386 -cc %GETH_MINGW%\bin\gcc.exe - env: - GETH_MINGW: 'C:\msys64\mingw32' + go run build/ci.go install -dlgo -os windows -arch 386 -cc i686-w64-mingw32-gcc - name: "Create/upload archive (386)" - shell: cmd run: | - go run build/ci.go archive -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds + go run build/ci.go archive -os windows -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Create/upload NSIS installer (386)" - shell: cmd run: | - set "PATH=C:\Program Files (x86)\NSIS;%PATH%" go run build/ci.go nsis -arch 386 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - del /Q build\bin\* + rm -f build/bin/* env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} diff --git a/accounts/abi/bind/old.go b/accounts/abi/bind/old.go index b09f5f3c7a..1fe1b1cca5 100644 --- a/accounts/abi/bind/old.go +++ b/accounts/abi/bind/old.go @@ -176,6 +176,13 @@ var ( // ErrNoCodeAfterDeploy is returned by WaitDeployed if contract creation leaves // an empty contract behind. ErrNoCodeAfterDeploy = bind2.ErrNoCodeAfterDeploy + + // ErrNoEventSignature is returned when a log entry has no topics. + ErrNoEventSignature = bind2.ErrNoEventSignature + + // ErrEventSignatureMismatch is returned when a log's topic[0] does not match + // the expected event signature. + ErrEventSignatureMismatch = bind2.ErrEventSignatureMismatch ) // ContractCaller defines the methods needed to allow operating with a contract on a read diff --git a/accounts/abi/unpack_test.go b/accounts/abi/unpack_test.go index 90713c03ca..90cfa68655 100644 --- a/accounts/abi/unpack_test.go +++ b/accounts/abi/unpack_test.go @@ -910,7 +910,7 @@ func TestUnpackTuple(t *testing.T) { }, }, FieldT: T{ - big.NewInt(0), big.NewInt(1), + big.NewInt(0).SetBits([]big.Word{}), big.NewInt(1), }, A: big.NewInt(1), } @@ -919,7 +919,7 @@ func TestUnpackTuple(t *testing.T) { if err != nil { t.Error(err) } - if reflect.DeepEqual(ret, expected) { + if !reflect.DeepEqual(ret, expected) { t.Error("unexpected unpack value") } } diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index aeafcfc838..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,39 +0,0 @@ -clone_depth: 5 -version: "{branch}.{build}" - -image: - - Visual Studio 2019 - -environment: - matrix: - - GETH_ARCH: amd64 - GETH_MINGW: 'C:\msys64\mingw64' - - GETH_ARCH: 386 - GETH_MINGW: 'C:\msys64\mingw32' - -install: - - git submodule update --init --depth 1 --recursive - - go version - -for: - # Windows builds for amd64 + 386. - - matrix: - only: - - image: Visual Studio 2019 - environment: - # We use gcc from MSYS2 because it is the most recent compiler version available on - # AppVeyor. Note: gcc.exe only works properly if the corresponding bin/ directory is - # contained in PATH. - GETH_CC: '%GETH_MINGW%\bin\gcc.exe' - PATH: '%GETH_MINGW%\bin;C:\Program Files (x86)\NSIS\;%PATH%' - build_script: - - 'echo %GETH_ARCH%' - - 'echo %GETH_CC%' - - '%GETH_CC% --version' - - go run build/ci.go install -dlgo -arch %GETH_ARCH% -cc %GETH_CC% - after_build: - # Upload builds. Note that ci.go makes this a no-op PR builds. - - go run build/ci.go archive -arch %GETH_ARCH% -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - - go run build/ci.go nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - test_script: - - go run build/ci.go test -dlgo -arch %GETH_ARCH% -cc %GETH_CC% -short diff --git a/beacon/engine/errors.go b/beacon/engine/errors.go index 62773a0ea9..80e13b11b9 100644 --- a/beacon/engine/errors.go +++ b/beacon/engine/errors.go @@ -81,6 +81,7 @@ var ( TooLargeRequest = &EngineAPIError{code: -38004, msg: "Too large request"} InvalidParams = &EngineAPIError{code: -32602, msg: "Invalid parameters"} UnsupportedFork = &EngineAPIError{code: -38005, msg: "Unsupported fork"} + TooDeepReorg = &EngineAPIError{code: -38006, msg: "Too deep reorg"} STATUS_INVALID = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: INVALID}, PayloadID: nil} STATUS_SYNCING = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: SYNCING}, PayloadID: nil} diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index c733b3f350..02a1fd3805 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -17,24 +18,25 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -60,30 +62,32 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber) + enc.BlockAccessList = e.BlockAccessList return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -160,5 +164,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.SlotNumber != nil { e.SlotNumber = (*uint64)(dec.SlotNumber) } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 8824e40f5e..7cc49c637e 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -82,24 +83,25 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - SlotNumber *uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. @@ -284,7 +286,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H if data.BaseFeePerGas != nil && (data.BaseFeePerGas.Sign() == -1 || data.BaseFeePerGas.BitLen() > 256) { return nil, fmt.Errorf("invalid baseFeePerGas: %v", data.BaseFeePerGas) } - var blobHashes = make([]common.Hash, 0, len(txs)) + var blobHashes = make([]common.Hash, 0, len(versionedHashes)) for _, tx := range txs { blobHashes = append(blobHashes, tx.BlobHashes()...) } @@ -311,56 +313,66 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H requestsHash = &h } - header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(data.Number), - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - WithdrawalsHash: withdrawalsRoot, - ExcessBlobGas: data.ExcessBlobGas, - BlobGasUsed: data.BlobGasUsed, - ParentBeaconRoot: beaconRoot, - RequestsHash: requestsHash, - SlotNumber: data.SlotNumber, + // If Amsterdam is enabled, data.BlockAccessList is always non-nil, + // even for empty blocks with no state transitions. + // + // If Amsterdam is not enabled yet, blockAccessListHash is expected + // to be nil. + var blockAccessListHash *common.Hash + if data.BlockAccessList != nil { + hash := data.BlockAccessList.Hash() + blockAccessListHash = &hash } - return types.NewBlockWithHeader(header). - WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), - nil + header := &types.Header{ + ParentHash: data.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: data.FeeRecipient, + Root: data.StateRoot, + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: data.ReceiptsRoot, + Bloom: types.BytesToBloom(data.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(data.Number), + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Time: data.Timestamp, + BaseFee: data.BaseFeePerGas, + Extra: data.ExtraData, + MixDigest: data.Random, + WithdrawalsHash: withdrawalsRoot, + ExcessBlobGas: data.ExcessBlobGas, + BlobGasUsed: data.BlobGasUsed, + ParentBeaconRoot: beaconRoot, + RequestsHash: requestsHash, + SlotNumber: data.SlotNumber, + BlockAccessListHash: blockAccessListHash, + } + return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil } // BlockToExecutableData constructs the ExecutableData structure by filling the // fields from the given block. It assumes the given block is post-merge block. func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope { data := &ExecutableData{ - BlockHash: block.Hash(), - ParentHash: block.ParentHash(), - FeeRecipient: block.Coinbase(), - StateRoot: block.Root(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - BaseFeePerGas: block.BaseFee(), - Timestamp: block.Time(), - ReceiptsRoot: block.ReceiptHash(), - LogsBloom: block.Bloom().Bytes(), - Transactions: encodeTransactions(block.Transactions()), - Random: block.MixDigest(), - ExtraData: block.Extra(), - Withdrawals: block.Withdrawals(), - BlobGasUsed: block.BlobGasUsed(), - ExcessBlobGas: block.ExcessBlobGas(), - SlotNumber: block.SlotNumber(), + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + Withdrawals: block.Withdrawals(), + BlobGasUsed: block.BlobGasUsed(), + ExcessBlobGas: block.ExcessBlobGas(), + SlotNumber: block.SlotNumber(), + BlockAccessList: block.AccessList(), } // Add blobs. diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go index 4fa87785c0..7fc735d893 100644 --- a/beacon/light/committee_chain.go +++ b/beacon/light/committee_chain.go @@ -182,6 +182,12 @@ func (s *CommitteeChain) Reset() { s.chainmu.Lock() defer s.chainmu.Unlock() + s.resetLocked() +} + +// ResetLocked resets the committee chain without locking. The caller should hold +// the chainmu lock. +func (s *CommitteeChain) resetLocked() { if err := s.rollback(0); err != nil { log.Error("Error writing batch into chain database", "error", err) } @@ -201,22 +207,22 @@ func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error { } period := bootstrap.Header.SyncPeriod() if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil { - s.Reset() + s.resetLocked() return err } if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil { - s.Reset() + s.resetLocked() if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil { - s.Reset() + s.resetLocked() return err } } if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil { - s.Reset() + s.resetLocked() return err } if err := s.addCommittee(period, bootstrap.Committee); err != nil { - s.Reset() + s.resetLocked() return err } s.changeCounter++ diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go index d84a3d64da..b15b967433 100644 --- a/beacon/light/sync/update_sync.go +++ b/beacon/light/sync/update_sync.go @@ -98,7 +98,10 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E case ssDefault: if resp != nil { if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) { - s.chain.CheckpointInit(*checkpoint) + err := s.chain.CheckpointInit(*checkpoint) + if err != nil { + return + } s.initialized = true return } diff --git a/build/checksums.txt b/build/checksums.txt index 1832ce41dd..454efa93c4 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,49 +5,49 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz -# version:golang 1.25.9 +# version:golang 1.25.10 # https://go.dev/dl/ -0ec9ef8ebcea097aac37decae9f09a7218b451cd96be7d6ed513d8e4bcf909cf go1.25.9.src.tar.gz -b9ede6378a8f8d3d22bf52e68beb69ef7abdb65929ab2456020383002da15846 go1.25.9.aix-ppc64.tar.gz -92cb78fba4796e218c1accb0ea0a214ef2094c382049a244ad6505505d015fbe go1.25.9.darwin-amd64.tar.gz -9528be7329b9770631a6bd09ca2f3a73ed7332bec01d87435e75e92d8f130363 go1.25.9.darwin-arm64.tar.gz -918e44a471c5524caa52f74185064240d5eb343aa8023d604776511fc7adffa6 go1.25.9.dragonfly-amd64.tar.gz -2d67dbdfd09c6fcaa0e64485367ef43b8837ea200c663d6417183237bcddf83d go1.25.9.freebsd-386.tar.gz -9152d0c0badbfeb0c0e148e47c12bec28099d8cf2db60958810c879e0b679d07 go1.25.9.freebsd-amd64.tar.gz -437dca59604ad4a806a6a88e3d7ec1cd98ac9b402a3671629f4e553dd8b9888f go1.25.9.freebsd-arm.tar.gz -4c0fe53977412036fc8081e8d0992bbaabe4d3e1926137271ba11c2f5753300f go1.25.9.freebsd-arm64.tar.gz -d6087cdd1c084bd186132f29e0d032852a745f3c7619003d0fd5612c1fa58c8a go1.25.9.freebsd-riscv64.tar.gz -f82e49037e195cb62beae6a6ad83497157b2af5a01bad2f1dcb65df41080aabb go1.25.9.illumos-amd64.tar.gz -1e14a73bc2b19e370e0d4c57ba87aabfe8aef1e435e14d246742d48a13254f36 go1.25.9.linux-386.tar.gz -00859d7bd6defe8bf84d9db9e57b9a4467b2887c18cd93ae7460e713db774bc1 go1.25.9.linux-amd64.tar.gz -ec342e7389b7f489564ed5463c63b16cf8040023dabc7861256677165a8c0e2b go1.25.9.linux-arm64.tar.gz -7d4f0d266d871301e08ef4ac31c56e66048688893b2848392e5c600276351ee8 go1.25.9.linux-armv6l.tar.gz -f3460d901a14496bc609636e4accf9110ee1869d41c64af7e29cd567cffcf49b go1.25.9.linux-loong64.tar.gz -1da96ea449382ff96c09c55cee74815324e01d687d5ac6d2ade58244b8574306 go1.25.9.linux-mips.tar.gz -311a7f5f01f9a4bd51288b575eb619dc8e28e1fbc0cd78256a428b3ca668ff01 go1.25.9.linux-mips64.tar.gz -0b4edaf9e2ba3f0a079547effda70ec6a4b51a6ca3271a1147652c87ebcf3735 go1.25.9.linux-mips64le.tar.gz -42667340df264896f20b12261429d954e736e9772ab83ba289e68c30cf6f9628 go1.25.9.linux-mipsle.tar.gz -b9cbb3a4894b5aca6966c23452608435e8535278ef019b18d8898fbbfab67e74 go1.25.9.linux-ppc64.tar.gz -b0c41c7da1fc8d39020d65296a0dc54167afd9f76d67064e22c31ce3d839a739 go1.25.9.linux-ppc64le.tar.gz -2a630be8f854177c13e5fa75f7812c721369ecb9bd6e4c0fb1bd1c708d08b37c go1.25.9.linux-riscv64.tar.gz -0cf55136ac7eaccfc36d849054f849510ea289c2d959ffbed7b3866b4f484d17 go1.25.9.linux-s390x.tar.gz -eaf8167ff10a6a3e5dd304ef5f2e020b3a7379e76fa1011dc49c895800bf367c go1.25.9.netbsd-386.tar.gz -3cc6a861e62e23feae660984e0f2f14a2efb5d1f655900afee1d51af98919ae4 go1.25.9.netbsd-amd64.tar.gz -c2c44dca10e882c30553f4aa2ab8f6722b670fb12882378c8f461a9105d40188 go1.25.9.netbsd-arm.tar.gz -f301b71a8ec448053a5d2597df2e178120204bc9a33266c81600dd5d020a61b4 go1.25.9.netbsd-arm64.tar.gz -c4543b7fdef9707b4896810c69b4160a43ecec210af45c300f3abd78aa0c9e72 go1.25.9.openbsd-386.tar.gz -37275325e314f5ab7cf8ae65c4efc7cbfdaf20b41c6849549739b57a3ac97544 go1.25.9.openbsd-amd64.tar.gz -f9c05b6b315e979ecdd47354dd287c01708d6a88dc6ae7af74c84df8fa00df94 go1.25.9.openbsd-arm.tar.gz -4e999f42cf959ff95ca84af1ea1db3771000f5e57e157904bc2ffc72c75e29a2 go1.25.9.openbsd-arm64.tar.gz -0c7fa6c7c2b1cc13ad32fa94fc31273b4adf39c1e0f0e5dcedac158ff526af3f go1.25.9.openbsd-ppc64.tar.gz -347b33953a4b6e8df17719296f360f60878fe48a2d482ceb3637a3dfd4950065 go1.25.9.openbsd-riscv64.tar.gz -889f77d567c06832e0d332fe2458653dc66d43cded7ddbca6f72ce0ca60029cc go1.25.9.plan9-386.tar.gz -978b1f931fadec2f2516237d2649ee845d93c8eaf47dd196cfd8d26c7b2706a1 go1.25.9.plan9-amd64.tar.gz -30b9565e5ad0a212fe00990ead700c751b416eb2ef8d7c91a204945a7ff83a48 go1.25.9.plan9-arm.tar.gz -9e9125ff84ab3c3522ec758cab9540a17e9cba12bfcc34b6bf556cb89b522591 go1.25.9.solaris-amd64.tar.gz -bf40515f5f4d834fa9ead31ff75581e61a38ac27bf49840b95c5c998d321c0f6 go1.25.9.windows-386.zip -a7a710e225467b34e9e09fb432b829c86c9b2da5821ee5418f7eb2e8ae1a22cc go1.25.9.windows-amd64.zip -33cd73cf1b3ceee655ef71bc96e94006c02ae3c617fdd67ac9be3dfae3957449 go1.25.9.windows-arm64.zip +20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz +a194e767c2ab4216a60acc068b9dbe6bf4fae05c14bb52d6bbdcb5b3ea521308 go1.25.10.aix-ppc64.tar.gz +52321165a3146cd91865ef98371506a846ed4dc4f9f1c9323e5ad90d2a411e06 go1.25.10.darwin-amd64.tar.gz +795691a425de7e7cdba3544f354dcd2cebcf52e87dc6898193878f34eb6d634f go1.25.10.darwin-arm64.tar.gz +e37b4544ba9e9e9a7ab2ed3116b3fc4d39a88da854baa5a566d9d6d3a9de7d4c go1.25.10.dragonfly-amd64.tar.gz +2a70d1fdabab637aa442ca94599a56e381238efa20cb995d5433b8579bfe482c go1.25.10.freebsd-386.tar.gz +9cdf522d87d47d82fec4a313cc4f8c3c94a7770426e8d443e4150a1f330cba71 go1.25.10.freebsd-amd64.tar.gz +6da6183633e9e59ffd9edefab68b5059c89b605596d94aaba650b1681fccd35f go1.25.10.freebsd-arm.tar.gz +7adcefeebdd05331f4d45f1ad2dddb5c53537cff6552e82f6595b3b833b95371 go1.25.10.freebsd-arm64.tar.gz +285f80a1ace21a7d94035cd753196eeada8cacd48e6396fd116ad5eb67aea957 go1.25.10.freebsd-riscv64.tar.gz +de7461bf0e5068a4f6e7f8713026d70516be6dbd5de5d21f9ced1c182f2f326e go1.25.10.illumos-amd64.tar.gz +2f574f2e2e19ead5b280fec0e7af5c81b76632685f03b6ac42dfa34c4b773c52 go1.25.10.linux-386.tar.gz +42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70 go1.25.10.linux-amd64.tar.gz +654da1f9b50a5d1c2a85ccf8ed405aa89c06e94d18384628bf186f7712677b08 go1.25.10.linux-arm64.tar.gz +39f168f158e693887d3ad006168af1b1a3007b19c5993cae4d9d57f82f52aaf8 go1.25.10.linux-armv6l.tar.gz +05401fe5ea50ad2bafb9c797ef9bf21574b0661f19ef4d0dd66af8a0fb7323f3 go1.25.10.linux-loong64.tar.gz +d5bc2d6155d394a3aae41f21eb7c60da5595a6147aa0f30ed6b27da25e06c3f7 go1.25.10.linux-mips.tar.gz +8c64e7493e5953c3ba3153487d2fddd7f8ed142392c77f138e6792a6c1930db4 go1.25.10.linux-mips64.tar.gz +bd53aa2d558b7c1eadfc6bf01132e1859203a92f458ed7ba75b7f3230f14b095 go1.25.10.linux-mips64le.tar.gz +120b254e2e2980bb06687175db5c4064a85696c53001dc9f59934ad18f74a6bc go1.25.10.linux-mipsle.tar.gz +8a6acb21295b0ec974a44608361920ea8dbff5666631a6f556bd7d5f1d56535f go1.25.10.linux-ppc64.tar.gz +778925fdcdf9a272f823d147fad51545c3334b7ccd8652b2ccaaf2b01800280a go1.25.10.linux-ppc64le.tar.gz +b4f04ad0db48bcfea946db5323919cd21034e0bd2821a557dacd29c1b1013a4b go1.25.10.linux-riscv64.tar.gz +936b953e43921a64c12da871f76871ebbeb6d2092a7b8bdc307f5246f3c662cc go1.25.10.linux-s390x.tar.gz +061470e0bc7132146a5925a3cc28d5bc498eb1b1ff09dedcfaae10f781ff2274 go1.25.10.netbsd-386.tar.gz +63b2d50d7f8f269a9c82d42a4060e90cffb7f9102299818bb071b067aac8da8f go1.25.10.netbsd-amd64.tar.gz +c35129f68796526aa4dc4b6f481e2d995ef312aedadc88b659b945cc00e1f8f0 go1.25.10.netbsd-arm.tar.gz +2f541da4e2b298154d992d1f11bbb38c89d0821d91cc50a46776d42bb5e63bca go1.25.10.netbsd-arm64.tar.gz +2d42e569b07f1b99fdbfd008e7c22f967d165e2ce02464f46818fbed2aec43f5 go1.25.10.openbsd-386.tar.gz +0ad05960e8c9f867328151308c87f938433bec8f22f6a9437a896e22169fc840 go1.25.10.openbsd-amd64.tar.gz +099cc11473f99461c77161912740945308f08f6834980afb262c72bdc915f2d7 go1.25.10.openbsd-arm.tar.gz +bdf3335d5008c1ddc81fa94892283e4f1fee22566f5351d4e726d9f55a67c838 go1.25.10.openbsd-arm64.tar.gz +0933d418da0a61e0f29de717a77498f16b9b5b50dbe2205e20b2ed7fd4067f75 go1.25.10.openbsd-ppc64.tar.gz +191e6f3e75712f8c13d189d53b668e2cac6449f26474c1d86fbd04f6e9846f9c go1.25.10.openbsd-riscv64.tar.gz +68c053c8acd76c50fc430e92f4a86110ec3d97dd03d27b9339b4eaf793caff5f go1.25.10.plan9-386.tar.gz +42e2c46638ae22d93402e79efb40faee5c42cf7c56a01bb3ab47c6bb2512b745 go1.25.10.plan9-amd64.tar.gz +3ef1d5838b1648da16724a07b72e839ccbd7cb8899c3e0426afd6b79d494b91c go1.25.10.plan9-arm.tar.gz +631e3716017fbec06500a628d97e1155daec3593f0a7812c2ebfe8fc8c96b2ab go1.25.10.solaris-amd64.tar.gz +ddc693d2d9d7cc671ebb72d1d50aa05670f95b059b7d90440611af57976871d5 go1.25.10.windows-386.zip +ca37af2dadd8544464f1a9ca7c3886499d1cdfcb263855d0a1d71f194b2bd222 go1.25.10.windows-amd64.zip +38be57e0398bd93673d65bcae6dc7ee3cf151d7038d0dba5c60a5153022872da go1.25.10.windows-arm64.zip # version:golangci 2.10.1 # https://github.com/golangci/golangci-lint/releases/ diff --git a/build/ci.go b/build/ci.go index 173288bcdc..173a3280ce 100644 --- a/build/ci.go +++ b/build/ci.go @@ -73,21 +73,9 @@ var ( "./cmd/keeper", } - // Files that end up in the geth*.zip archive. - gethArchiveFiles = []string{ - "COPYING", - executablePath("geth"), - } - - // Files that end up in the geth-alltools*.zip archive. - allToolsArchiveFiles = []string{ - "COPYING", - executablePath("abigen"), - executablePath("evm"), - executablePath("geth"), - executablePath("rlpdump"), - executablePath("clef"), - } + // Files that end up in the geth-alltools*.zip archive (and the NSIS installer + // dev-tools section). Order matches the historical layout produced by ci.go. + allToolsBinaries = []string{"abigen", "evm", "geth", "rlpdump", "clef"} // Keeper build targets with their configurations keeperTargets = []struct { @@ -180,13 +168,35 @@ var ( var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin")) -func executablePath(name string) string { - if runtime.GOOS == "windows" { +// executablePath returns the path to a built binary in GOBIN, applying the +// platform-specific extension for the given target OS. +func executablePath(name, targetOS string) string { + if targetOS == "windows" { name += ".exe" } return filepath.Join(GOBIN, name) } +// gethArchiveFiles returns the file list for the geth-{platform}-{ver}.zip +// archive, with binary paths resolved for the target OS. +func gethArchiveFiles(targetOS string) []string { + return []string{ + "COPYING", + executablePath("geth", targetOS), + } +} + +// allToolsArchiveFiles returns the file list for the +// geth-alltools-{platform}-{ver}.zip archive, with binary paths resolved for +// the target OS. +func allToolsArchiveFiles(targetOS string) []string { + files := []string{"COPYING"} + for _, name := range allToolsBinaries { + files = append(files, executablePath(name, targetOS)) + } + return files +} + func main() { log.SetFlags(log.Lshortfile) @@ -233,6 +243,7 @@ func main() { func doInstall(cmdline []string) { var ( dlgo = flag.Bool("dlgo", false, "Download Go and build with it") + targetOS = flag.String("os", runtime.GOOS, "Target OS to cross build for") arch = flag.String("arch", "", "Architecture to cross build for") cc = flag.String("cc", "", "C compiler to cross build with") staticlink = flag.Bool("static", false, "Create statically-linked executable") @@ -241,7 +252,7 @@ func doInstall(cmdline []string) { env := build.Env() // Configure the toolchain. - tc := build.GoToolchain{GOARCH: *arch, CC: *cc} + tc := build.GoToolchain{GOOS: *targetOS, GOARCH: *arch, CC: *cc} if *dlgo { csdb := download.MustLoadChecksums("build/checksums.txt") tc.Root = build.DownloadGo(csdb) @@ -255,7 +266,7 @@ func doInstall(cmdline []string) { } // Configure the build. - gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags)...) + gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags, *targetOS)...) // Show packages during build. gobuild.Args = append(gobuild.Args, "-v") @@ -270,7 +281,7 @@ func doInstall(cmdline []string) { // Do the build! for _, pkg := range packages { args := slices.Clone(gobuild.Args) - args = append(args, "-o", executablePath(path.Base(pkg))) + args = append(args, "-o", executablePath(path.Base(pkg), *targetOS)) args = append(args, pkg) build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env}) } @@ -297,7 +308,13 @@ func doInstallKeeper(cmdline []string) { tc.GOARCH = target.GOARCH tc.GOOS = target.GOOS tc.CC = target.CC - gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags})...) + // An empty GOOS means "build for the host OS"; thread that through to + // buildFlags so platform-specific linker flags are picked correctly. + targetOS := target.GOOS + if targetOS == "" { + targetOS = runtime.GOOS + } + gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags}, targetOS)...) gobuild.Dir = "./cmd/keeper" gobuild.Args = append(gobuild.Args, "-v") @@ -307,14 +324,15 @@ func doInstallKeeper(cmdline []string) { outputName := fmt.Sprintf("keeper-%s", target.Name) args := slices.Clone(gobuild.Args) - args = append(args, "-o", executablePath(outputName)) + args = append(args, "-o", executablePath(outputName, targetOS)) args = append(args, ".") build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env, Dir: gobuild.Dir}) } } -// buildFlags returns the go tool flags for building. -func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (flags []string) { +// buildFlags returns the go tool flags for building. targetOS is the OS we +// are producing binaries for. +func buildFlags(env build.Environment, staticLinking bool, buildTags []string, targetOS string) (flags []string) { var ld []string // See https://github.com/golang/go/issues/33772#issuecomment-528176001 // We need to set --buildid to the linker here, and also pass --build-id to the @@ -326,10 +344,10 @@ func buildFlags(env build.Environment, staticLinking bool, buildTags []string) ( } // Strip DWARF on darwin. This used to be required for certain things, // and there is no downside to this, so we just keep doing it. - if runtime.GOOS == "darwin" { + if targetOS == "darwin" { ld = append(ld, "-s") } - if runtime.GOOS == "linux" { + if targetOS == "linux" { // Enforce the stacksize to 8M, which is the case on most platforms apart from // alpine Linux. // See https://sourceware.org/binutils/docs-2.23.1/ld/Options.html#Options @@ -682,12 +700,13 @@ func downloadProtoc(cachedir string) string { // Release Packaging func doArchive(cmdline []string) { var ( - arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging") - atype = flag.String("type", "zip", "Type of archive to write (zip|tar)") - signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`) - signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`) - upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) - ext string + targetOS = flag.String("os", runtime.GOOS, "Target OS the binaries were built for") + arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging") + atype = flag.String("type", "zip", "Type of archive to write (zip|tar)") + signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`) + signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`) + upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) + ext string ) flag.CommandLine.Parse(cmdline) switch *atype { @@ -701,15 +720,15 @@ func doArchive(cmdline []string) { var ( env = build.Env() - basegeth = archiveBasename(*arch, version.Archive(env.Commit)) + basegeth = archiveBasename(*targetOS, *arch, version.Archive(env.Commit)) geth = "geth-" + basegeth + ext alltools = "geth-alltools-" + basegeth + ext ) maybeSkipArchive(env) - if err := build.WriteArchive(geth, gethArchiveFiles); err != nil { + if err := build.WriteArchive(geth, gethArchiveFiles(*targetOS)); err != nil { log.Fatal(err) } - if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil { + if err := build.WriteArchive(alltools, allToolsArchiveFiles(*targetOS)); err != nil { log.Fatal(err) } for _, archive := range []string{geth, alltools} { @@ -735,7 +754,11 @@ func doKeeperArchive(cmdline []string) { maybeSkipArchive(env) files := []string{"COPYING"} for _, target := range keeperTargets { - files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name))) + targetOS := target.GOOS + if targetOS == "" { + targetOS = runtime.GOOS + } + files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name), targetOS)) } if err := build.WriteArchive(keeper, files); err != nil { log.Fatal(err) @@ -745,8 +768,8 @@ func doKeeperArchive(cmdline []string) { } } -func archiveBasename(arch string, archiveVersion string) string { - platform := runtime.GOOS + "-" + arch +func archiveBasename(targetOS, arch, archiveVersion string) string { + platform := targetOS + "-" + arch if arch == "arm" { platform += os.Getenv("GOARM") } @@ -1209,13 +1232,13 @@ func doWindowsInstaller(cmdline []string) { env := build.Env() maybeSkipArchive(env) - // Aggregate binaries that are included in the installer + // Aggregate binaries that are included in the installer. var ( devTools []string allTools []string gethTool string ) - for _, file := range allToolsArchiveFiles { + for _, file := range allToolsArchiveFiles("windows") { if file == "COPYING" { // license, copied later continue } @@ -1252,16 +1275,24 @@ func doWindowsInstaller(cmdline []string) { if env.Commit != "" { ver[2] += "-" + env.Commit[:8] } - installer, err := filepath.Abs("geth-" + archiveBasename(*arch, version.Archive(env.Commit)) + ".exe") + installer, err := filepath.Abs("geth-" + archiveBasename("windows", *arch, version.Archive(env.Commit)) + ".exe") if err != nil { log.Fatalf("Failed to convert installer file path: %v", err) } - build.MustRunCommand("makensis.exe", - "/DOUTPUTFILE="+installer, - "/DMAJORVERSION="+ver[0], - "/DMINORVERSION="+ver[1], - "/DBUILDVERSION="+ver[2], - "/DARCH="+*arch, + // makensis on Windows is "makensis.exe" with /D-style defines; on Linux + // (and other Unixes) the binary is "makensis" and accepts -D. + makensisCmd := "makensis" + defineFlag := "-D" + if runtime.GOOS == "windows" { + makensisCmd = "makensis.exe" + defineFlag = "/D" + } + build.MustRunCommand(makensisCmd, + defineFlag+"OUTPUTFILE="+installer, + defineFlag+"MAJORVERSION="+ver[0], + defineFlag+"MINORVERSION="+ver[1], + defineFlag+"BUILDVERSION="+ver[2], + defineFlag+"ARCH="+*arch, filepath.Join(*workdir, "geth.nsi"), ) // Sign and publish installer. diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index c82358be49..d9d1fa02ac 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -215,7 +215,7 @@ func generate(c *cli.Context) error { code string err error ) - if c.IsSet(v2Flag.Name) { + if c.Bool(v2Flag.Name) { code, err = abigen.BindV2(types, abis, bins, c.String(pkgFlag.Name), libs, aliases) } else { code, err = abigen.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), libs, aliases) diff --git a/cmd/devp2p/enrcmd.go b/cmd/devp2p/enrcmd.go index c9b692612f..af2cf90a81 100644 --- a/cmd/devp2p/enrcmd.go +++ b/cmd/devp2p/enrcmd.go @@ -194,7 +194,7 @@ func formatAttrString(v rlp.RawValue) (string, bool) { func formatAttrIP(v rlp.RawValue) (string, bool) { content, _, err := rlp.SplitString(v) - if err != nil || len(content) != 4 && len(content) != 6 { + if err != nil || len(content) != 4 && len(content) != 16 { return "", false } return net.IP(content).String(), true diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 253ebe1111..043e675494 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -17,6 +17,7 @@ package t8ntool import ( + "context" "encoding/json" "fmt" stdmath "math" @@ -34,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/ethdb" @@ -171,6 +173,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, includedTxs types.Transactions blobGasUsed = uint64(0) receipts = make(types.Receipts, 0) + + // TODO return blockAccessList as a part of result + blockAccessList = bal.NewConstructionBlockAccessList() ) vmContext := vm.BlockContext{ CanTransfer: core.CanTransfer, @@ -230,14 +235,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) + core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) } if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) { var ( prevNumber = pre.Env.Number - 1 prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)] ) - core.ProcessParentBlockHash(prevHash, evm) + core.ProcessParentBlockHash(prevHash, evm, blockAccessList) } for i := 0; txIt.Next(); i++ { tx, err := txIt.Tx() @@ -269,12 +274,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, continue } } - statedb.SetTxContext(tx.Hash(), len(receipts)) + statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1)) + var ( snapshot = statedb.Snapshot() gp = gaspool.Snapshot() ) - receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) + receipt, bal, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) @@ -291,6 +297,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } blobGasUsed += txBlobGas receipts = append(receipts, receipt) + blockAccessList.Merge(bal) } statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)) @@ -331,26 +338,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } // Gather the execution-layer triggered requests. - var requests [][]byte - if chainConfig.IsPrague(vmContext.BlockNumber, vmContext.Time) { - requests = [][]byte{} - // EIP-6110 - var allLogs []*types.Log - for _, receipt := range receipts { - allLogs = append(allLogs, receipt.Logs...) - } - if err := core.ParseDepositLogs(&requests, allLogs, chainConfig); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err)) - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process withdrawal requests: %v", err)) - } - // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process consolidation requests: %v", err)) - } + var allLogs []*types.Log + for _, receipt := range receipts { + allLogs = append(allLogs, receipt.Logs...) } + requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) + if err != nil { + return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) + } + blockAccessList.Merge(bal) // Commit block root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time)) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 7e068c06af..ca19ae3386 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { r.Error = err results = append(results, r) @@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { r.Error = err results = append(results, r) diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index e0bb3a449d..89b703d3b8 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -546,7 +546,7 @@ func BinKeys(ctx *cli.Context) error { db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults) defer db.Close() - bt, err := genBinTrieFromAlloc(alloc, db) + bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth) if err != nil { return fmt.Errorf("error generating bt: %w", err) } @@ -590,7 +590,7 @@ func BinTrieRoot(ctx *cli.Context) error { db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults) defer db.Close() - bt, err := genBinTrieFromAlloc(alloc, db) + bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth) if err != nil { return fmt.Errorf("error generating bt: %w", err) } @@ -600,8 +600,8 @@ func BinTrieRoot(ctx *cli.Context) error { } // TODO(@CPerezz): Should this go to `bintrie` module? -func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase) (*bintrie.BinaryTrie, error) { - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db) +func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase, groupDepth int) (*bintrie.BinaryTrie, error) { + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db, groupDepth) if err != nil { return nil, err } diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go index 82e7bdff3d..6d80056d04 100644 --- a/cmd/evm/runner.go +++ b/cmd/evm/runner.go @@ -321,7 +321,7 @@ func runCmd(ctx *cli.Context) error { // don't mutate the state! runtimeConfig.State = prestate.Copy() output, _, gasLeft, err := runtime.Create(input, &runtimeConfig) - return output, gasLeft, err + return output, initialGas - gasLeft, err } } else { if len(code) > 0 { diff --git a/cmd/geth/bintrie_convert.go b/cmd/geth/bintrie_convert.go index 43d2e629ac..46cb3aa7e4 100644 --- a/cmd/geth/bintrie_convert.go +++ b/cmd/geth/bintrie_convert.go @@ -151,7 +151,7 @@ func convertToBinaryTrie(ctx *cli.Context) error { }) defer destTriedb.Close() - binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, ctx.Int(utils.BinTrieGroupDepthFlag.Name)) if err != nil { return fmt.Errorf("failed to create binary trie: %w", err) } @@ -319,7 +319,7 @@ func commitBinaryTrie(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *t runtime.GC() debug.FreeOSMemory() - bt, err := bintrie.NewBinaryTrie(newRoot, destDB) + bt, err := bintrie.NewBinaryTrie(newRoot, destDB, bt.GroupDepth()) if err != nil { return nil, common.Hash{}, fmt.Errorf("failed to reload binary trie: %w", err) } diff --git a/cmd/geth/bintrie_convert_test.go b/cmd/geth/bintrie_convert_test.go index 50ae752358..32e8c7e55b 100644 --- a/cmd/geth/bintrie_convert_test.go +++ b/cmd/geth/bintrie_convert_test.go @@ -87,7 +87,7 @@ func TestBintrieConvert(t *testing.T) { }) defer destTriedb.Close() - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8) if err != nil { t.Fatalf("failed to create binary trie: %v", err) } @@ -98,7 +98,7 @@ func TestBintrieConvert(t *testing.T) { } t.Logf("Binary trie root: %x", currentRoot) - bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb) + bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb, 8) if err != nil { t.Fatalf("failed to reload binary trie: %v", err) } @@ -194,7 +194,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) { PathDB: pathdb.Defaults, }) - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8) if err != nil { t.Fatalf("failed to create binary trie: %v", err) } @@ -209,7 +209,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) { } srcTriedb2.Close() - bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb) + bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb, 8) if err != nil { t.Fatalf("failed to reload binary trie after deletion: %v", err) } diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 0aacb0878a..98ed348d8c 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -325,7 +325,7 @@ func dumpGenesis(ctx *cli.Context) error { var genesis *core.Genesis if utils.IsNetworkPreset(ctx) { genesis = utils.MakeGenesis(ctx) - } else if ctx.IsSet(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) { + } else if ctx.Bool(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) { genesis = core.DeveloperGenesisBlock(11_500_000, nil) } diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8e2db32d76..c02e307bdc 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/eth/syncer" "github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/internal/telemetry/tracesetup" "github.com/ethereum/go-ethereum/internal/version" @@ -269,25 +270,28 @@ func makeFullNode(ctx *cli.Context) *node.Node { filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth) // Configure GraphQL if requested. - if ctx.IsSet(utils.GraphQLEnabledFlag.Name) { + if ctx.Bool(utils.GraphQLEnabledFlag.Name) { utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node) } // Add the Ethereum Stats daemon if requested. if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL) } + // Configure synchronization override service - var synctarget common.Hash + syncConfig := syncer.Config{ + ExitWhenSynced: ctx.Bool(utils.ExitWhenSyncedFlag.Name), + } if ctx.IsSet(utils.SyncTargetFlag.Name) { target := ctx.String(utils.SyncTargetFlag.Name) if !common.IsHexHash(target) { utils.Fatalf("sync target hash is not a valid hex hash: %s", target) } - synctarget = common.HexToHash(target) + syncConfig.TargetBlock = common.HexToHash(target) } - utils.RegisterSyncOverrideService(stack, eth, synctarget, ctx.Bool(utils.ExitWhenSyncedFlag.Name)) + utils.RegisterSyncOverrideService(stack, eth, syncConfig) - if ctx.IsSet(utils.DeveloperFlag.Name) { + if ctx.Bool(utils.DeveloperFlag.Name) { // Start dev mode. simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), cfg.Eth.Miner.PendingFeeRecipient, eth) if err != nil { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ae869ec970..850e26d161 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -22,13 +22,10 @@ import ( "os" "slices" "sort" - "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console/prompt" - "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/flags" @@ -95,6 +92,7 @@ var ( utils.StateHistoryFlag, utils.TrienodeHistoryFlag, utils.TrienodeHistoryFullValueCheckpointFlag, + utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, utils.LegacyWhitelistFlag, // deprecated @@ -386,28 +384,4 @@ func startNode(ctx *cli.Context, stack *node.Node, isConsole bool) { } } }() - - // Spawn a standalone goroutine for status synchronization monitoring, - // close the node when synchronization is complete if user required. - if ctx.Bool(utils.ExitWhenSyncedFlag.Name) { - go func() { - sub := stack.EventMux().Subscribe(downloader.DoneEvent{}) - defer sub.Unsubscribe() - for { - event := <-sub.Chan() - if event == nil { - continue - } - done, ok := event.Data.(downloader.DoneEvent) - if !ok { - continue - } - if timestamp := time.Unix(int64(done.Latest.Time), 0); time.Since(timestamp) < 10*time.Minute { - log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(), - "age", common.PrettyAge(timestamp)) - stack.Close() - } - } - }() - } } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9d996f15cb..c41cf4ee40 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -297,6 +297,12 @@ var ( Value: ethconfig.Defaults.EnableStateSizeTracking, Category: flags.StateCategory, } + BinTrieGroupDepthFlag = &cli.IntFlag{ + Name: "bintrie.groupdepth", + Usage: "Number of levels per serialized group in binary trie (1-8, default 5). Lower values create smaller groups with more nodes.", + Value: 5, + Category: flags.StateCategory, + } StateHistoryFlag = &cli.Uint64Flag{ Name: "history.state", Usage: "Number of recent blocks to retain state history for, only relevant in state.scheme=path (default = 90,000 blocks, 0 = entire chain)", @@ -1098,7 +1104,7 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. RPCTelemetrySampleRatioFlag = &cli.Float64Flag{ Name: "rpc.telemetry.sample-ratio", Usage: "Defines the sampling ratio for RPC telemetry (0.0 to 1.0)", - Value: 1.0, + Value: node.DefaultConfig.OpenTelemetry.SampleRatio, Category: flags.APICategory, } // Era flags are a group of flags related to the era archive format. @@ -1817,6 +1823,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(TrienodeHistoryFullValueCheckpointFlag.Name) { cfg.NodeFullValueCheckpoint = uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)) } + if ctx.IsSet(BinTrieGroupDepthFlag.Name) { + cfg.BinTrieGroupDepth = ctx.Int(BinTrieGroupDepthFlag.Name) + } if ctx.IsSet(StateSchemeFlag.Name) { cfg.StateScheme = ctx.String(StateSchemeFlag.Name) } @@ -2228,13 +2237,13 @@ func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconf } // RegisterSyncOverrideService adds the synchronization override service into node. -func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, target common.Hash, exitWhenSynced bool) { - if target != (common.Hash{}) { - log.Info("Registered sync override service", "hash", target, "exitWhenSynced", exitWhenSynced) +func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, config syncer.Config) { + if config.TargetBlock != (common.Hash{}) { + log.Info("Registered sync override service", "hash", config.TargetBlock, "exitWhenSynced", config.ExitWhenSynced) } else { log.Info("Registered sync override service") } - syncer.Register(stack, eth, target, exitWhenSynced) + syncer.Register(stack, eth, config) } // SetupMetrics configures the metrics system. @@ -2433,6 +2442,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh StateHistory: ctx.Uint64(StateHistoryFlag.Name), TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name), NodeFullValueCheckpoint: uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)), + BinTrieGroupDepth: ctx.Int(BinTrieGroupDepthFlag.Name), // Disable transaction indexing/unindexing. TxLookupLimit: -1, diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 6b9f412078..c00cd879c8 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -204,6 +204,10 @@ func (b *Big) ToInt() *big.Int { return (*big.Int)(b) } +func (b *Big) ToUint256() (*uint256.Int, bool) { + return uint256.FromBig((*big.Int)(b)) +} + // String returns the hex encoding of b. func (b *Big) String() string { return EncodeBig(b.ToInt()) diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 72ac75c036..4237418e73 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -342,9 +343,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { if !beacon.IsPoSHeader(header) { - beacon.ethone.Finalize(chain, header, state, body) + beacon.ethone.Finalize(chain, header, state, body, blockAccessIndex, bal) return } // Withdrawals processing. @@ -352,7 +353,20 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // Convert amount from gwei to wei. amount := new(uint256.Int).SetUint64(w.Amount) amount = amount.Mul(amount, uint256.NewInt(params.GWei)) - state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + + // Populate the block-level accessList if Amsterdam is enabled + if chain.Config().IsAmsterdam(header.Number, header.Time) { + if w.Amount == 0 { + // Zero amount withdrawal, account is accessed potential + // without state changes. + bal.AccountRead(w.Address) + } else { + // Non-zero amount withdrawal, account is accessed with + // a balance change. + bal.BalanceChange(blockAccessIndex, w.Address, new(uint256.Int).Add(&prev, amount)) + } + } } // No block reward which is issued by consensus layer instead. } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index ceaec44656..f44afde241 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/keccak" @@ -573,7 +574,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Finalize implements consensus.Engine. There is no post-transaction // consensus rules in clique, do nothing here. -func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // No block rewards in PoA, so the state remains as is } diff --git a/consensus/consensus.go b/consensus/consensus.go index 4ba389292f..e4f7b7a6a1 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" ) @@ -79,12 +80,12 @@ type Engine interface { // rules of a particular engine. The changes are executed inline. Prepare(chain ChainHeaderReader, header *types.Header) error - // Finalize runs any post-transaction state modifications (e.g. block rewards - // or process withdrawals) but does not assemble the block. + // Finalize runs any post-transaction consensus-specific state modifications + // (e.g. block rewards or process withdrawals) but does not assemble the block. // // Note: The state database might be updated to reflect any consensus rules // that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) + Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index ee9d9d97d6..21adc9d279 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/params" @@ -504,7 +505,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine, accumulating the block and uncle rewards. -func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // Accumulate any block and uncle rewards accumulateRewards(chain.Config(), state, header, body.Uncles) } diff --git a/core/bal_test.go b/core/bal_test.go new file mode 100644 index 0000000000..f0b9dc6443 --- /dev/null +++ b/core/bal_test.go @@ -0,0 +1,1319 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "crypto/ecdsa" + "maps" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// EIP-7928 BAL inclusion tests. +// +// Each test exercises a single rule from the spec and asserts both presence +// and absence in the resulting block access list. + +// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis. +func balChainConfig() *params.ChainConfig { + cfg := *params.MergedTestChainConfig + cfg.AmsterdamTime = new(uint64) + blob := *cfg.BlobScheduleConfig + blob.Amsterdam = blob.Osaka + cfg.BlobScheduleConfig = &blob + return &cfg +} + +// balTestEnv bundles common identities used across the tests. +type balTestEnv struct { + cfg *params.ChainConfig + signer types.Signer + key *ecdsa.PrivateKey + from common.Address + gspec *Genesis +} + +// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys +// the EIP-7928 system contracts. Extra accounts can be merged into Alloc. +func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv { + cfg := balChainConfig() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + from := crypto.PubkeyToAddress(key.PublicKey) + + alloc := types.GenesisAlloc{ + from: {Balance: newGwei(1_000_000_000)}, + params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0}, + params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0}, + params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0}, + params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0}, + } + maps.Copy(alloc, extra) + return &balTestEnv{ + cfg: cfg, + signer: types.LatestSigner(cfg), + key: key, + from: from, + gspec: &Genesis{Config: cfg, Alloc: alloc}, + } +} + +// run generates exactly one Amsterdam block and returns its BAL. +func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) { + t.Helper() + engine := beacon.New(ethash.NewFaker()) + _, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) { + gen(b) + }) + if blocks[0].AccessList() == nil { + t.Fatal("expected non-nil block access list") + } + return blocks[0].AccessList(), receipts[0] +} + +// --- assertion helpers --- + +func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + for i := range *b { + if (*b)[i].Address == addr { + return &(*b)[i] + } + } + return nil +} + +func hasSlotIn(slots []*uint256.Int, key common.Hash) bool { + want := new(uint256.Int).SetBytes(key[:]) + for _, s := range slots { + if s.Cmp(want) == 0 { + return true + } + } + return false +} + +func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool { + aa := findAccount(b, addr) + if aa == nil { + return false + } + want := new(uint256.Int).SetBytes(key[:]) + for _, w := range aa.StorageWrites { + if w.Slot.Cmp(want) == 0 { + return true + } + } + return false +} + +func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + t.Helper() + aa := findAccount(b, addr) + if aa == nil { + t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint()) + } + return aa +} + +func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) { + t.Helper() + if findAccount(b, addr) != nil { + t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint()) + } +} + +func assertEmpty(t *testing.T, aa *bal.AccountAccess) { + t.Helper() + if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 || + len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 { + t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa) + } +} + +// --- tx builders --- + +func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction { + return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{ + ChainID: e.cfg.ChainID, + Nonce: nonce, + To: to, + Value: value, + Gas: gas, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(tipGwei), + Data: data, + }) +} + +// ============================== Account inclusion ============================== + +// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender +// and a balance entry for the recipient. +func TestBALTxSenderAndRecipient(t *testing.T) { + to := common.HexToAddress("0xc0ffee") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender missing balance change") + } + recipient := assertPresent(t, b, to) + if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("recipient balance: %+v", recipient.BalanceChanges) + } +} + +// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient, +// but without a balance entry. +func TestBALZeroValueRecipient(t *testing.T) { + to := common.HexToAddress("0x0123456789abcdef") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + + r := assertPresent(t, b, to) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges) + } +} + +// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals) +// never touches the coinbase, so it must NOT appear in the BAL — the zero +// block reward alone does not trigger inclusion. +func TestBALEmptyBlockExcludesCoinbase(t *testing.T) { + coinbase := common.Address{0xc0} + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal but does not record any access. + g.SetCoinbase(coinbase) + }) + assertAbsent(t, b, coinbase) +} + +// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase +// and the balance change appears in the BAL. +func TestBALCoinbaseTipCapturesBalance(t *testing.T) { + coinbase := common.Address{0xc0} + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(coinbase) + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil)) + }) + + cb := assertPresent(t, b, coinbase) + if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges) + } +} + +// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL +// for a regular block. +func TestBALSystemAddressExcluded(t *testing.T) { + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + assertAbsent(t, b, params.SystemAddress) +} + +// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular +// account in the BAL once it experiences state access (here: receives value). +func TestBALSystemAddressIncludedWhenTouched(t *testing.T) { + sys := params.SystemAddress + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + aa := assertPresent(t, b, sys) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked +// indirectly — via STATICCALL from a regular contract — must still appear in +// the BAL with no balance entry. +func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + caller := common.HexToAddress("0xca11") + // PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff) + // PUSH20 0x04 GAS STATICCALL POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, identity.Bytes()...) + code = append(code, 0x5a, 0xfa, 0x50, 0x00) + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile +// with zero value lists the precompile but records no balance entry. +func TestBALPrecompileCalledNoValueIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad})) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only +// in the form of a value transfer — the balance entry is then recorded. +func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 { + t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges) + } +} + +// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address +// still adds it to the BAL with an empty change set. +func TestBALBalanceProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed") + caller := common.HexToAddress("0xc1") + // PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset) + // PUSH20 probe EXTCODECOPY STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, probe.Bytes()...) + code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is +// never actually touched must NOT appear in the BAL. +func TestBALAccessListNotAutoPromoted(t *testing.T) { + to := common.HexToAddress("0xabba") + dormant := common.HexToAddress("0xd0d0") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{ + ChainID: env.cfg.ChainID, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + Gas: params.TxGas + 4000, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(0), + AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, dormant) +} + +// ============================== CALL family ============================== + +// makeStubCaller emits a single CALL-family op against `target` then STOPs, +// with zero call data and discarded return data. +// +// op = 0xf1 (CALL) / 0xf2 (CALLCODE): +// stack = retSize, retOff, argsSize, argsOff, value, addr, gas +// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL): +// stack = retSize, retOff, argsSize, argsOff, addr, gas +func makeStubCaller(op byte, target common.Address) []byte { + // retSize, retOff, argsSize, argsOff = 0 + prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00} + if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0 + prelude = append(prelude, 0x60, 0x00) + } + prelude = append(prelude, 0x73) // PUSH20 + prelude = append(prelude, target.Bytes()...) + prelude = append(prelude, 0x5a) // GAS + prelude = append(prelude, op) + prelude = append(prelude, 0x50, 0x00) // POP, STOP + return prelude +} + +// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing +// contract that has no state changes lists the target with empty entries. +func TestBALCallTargetWithEmptyChangeSet(t *testing.T) { + target := common.HexToAddress("0xbabe") + env := newBALTestEnv(types.GenesisAlloc{ + target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an +// empty change set (CALLCODE executes target's code in the caller's storage +// context, so the target itself records no state changes). +func TestBALCallCodeTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target +// in the BAL even when neither produces state changes. +func TestBALDelegateCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with +// no balance entry recorded. +func TestBALStaticCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// ============================== Revert behaviour ============================== + +// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still +// records the touched contract in the BAL with an empty change set. +func TestBALRevertedTxStillIncluded(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + // PUSH1 0 PUSH1 0 REVERT + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, reverter)) +} + +// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the +// sender's final nonce and balance MUST be recorded. +func TestBALSenderRecordedOnRevert(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender balance change (gas paid) must be present on revert") + } +} + +// ============================== Storage inclusion ============================== + +// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and +// keeps it out of storage_reads. +func TestBALStorageWriteRecorded(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x01)) + // PUSH1 0x42 PUSH1 0x01 SSTORE STOP + code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot 0x01 must NOT appear in storage_reads") + } +} + +// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads. +func TestBALStorageSloadOnly(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x07)) + // PUSH1 0x07 SLOAD POP STOP + code := []byte{0x60, 0x07, 0x54, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must NOT appear in storage_changes") + } +} + +// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the +// same slot drops the slot from storage_reads (write-wins invariant). +func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x05)) + // PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP + code := []byte{ + 0x60, 0x05, 0x54, 0x50, + 0x60, 0x42, 0x60, 0x05, 0x55, + 0x00, + } + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint()) + } +} + +// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed +// value lands the slot in storage_reads only. +func TestBALNoOpSSTOREDemotesToRead(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x09)) + // SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op. + code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("no-op SSTORE must NOT register a write") + } +} + +// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a +// state change and lands in storage_changes. +func TestBALStorageWriteZeroIsAWrite(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x03)) + // PUSH1 0 PUSH1 3 SSTORE STOP + code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint()) + } + for _, w := range aa.StorageWrites { + if w.Slot.Uint64() == 0x03 { + if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() { + t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses) + } + } + } +} + +// ============================== CREATE / contract deployment ============================== + +// TestBALCreateDeploysCode: a successful contract-creation tx records the new +// address with nonce 0→1, a balance entry (value transferred), and a code entry. +func TestBALCreateDeploysCode(t *testing.T) { + env := newBALTestEnv(nil) + // Init: deploy runtime [0x00] (single STOP byte). + // PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN + init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) { + t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges) + } + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 { + t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges) + } +} + +// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the +// new address is still listed with nonce 0→1 but no code entry. +func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + // Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges) + } +} + +// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be +// contract address is in the BAL with an empty change set. +func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // PUSH1 0 PUSH1 0 REVERT + init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves +// the deployed address in the BAL with an empty change set. +func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. + init := []byte{0x5b, 0x60, 0x00, 0x56} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address +// that already holds a contract, the deployment fails but the address was +// probed during execution and MUST appear in the BAL with an empty change set. +func TestBALCreateAddressCollisionStillIncluded(t *testing.T) { + env := newBALTestEnv(nil) + // For a top-level CREATE tx the deployed address is CreateAddress(sender, 0). + // Pre-allocate a contract at that address to provoke ErrContractAddressCollision. + collide := crypto.CreateAddress(env.from, 0) + env.gspec.Alloc[collide] = types.Account{ + Nonce: 1, + Code: []byte{0x00}, + Balance: common.Big0, + } + + // Init code doesn't matter — execution never starts. + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + aa := assertPresent(t, b, collide) + // The address must be present but the pre-existing nonce/code MUST NOT + // be overwritten by the failed creation. + if len(aa.NonceChanges) != 0 { + t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("collision must not write code: %+v", aa.CodeChanges) + } +} + +// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame +// aborts BEFORE the destination is read from state (here: the caller has 0 +// balance and CREATE requests value > 0, tripping evm.create's CanTransfer +// check before GetCodeHash), the would-be address MUST NOT appear in the +// BAL — only "if target account is accessed" qualifies for inclusion. +func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil)) + }) + + // The address that WOULD have been deployed had the create succeeded. + wouldBeDest := crypto.CreateAddress(factory, 1) + assertAbsent(t, b, wouldBeDest) + + // The factory itself is in BAL (it ran), but its nonce MUST NOT have been + // bumped because evm.create returned before the SetNonce call. + aa := assertPresent(t, b, factory) + if len(aa.NonceChanges) != 0 { + t.Fatalf("factory nonce must not be bumped on pre-access abort: %+v", aa.NonceChanges) + } +} + +// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract +// (not a top-level CREATE tx) records the deployed address in the BAL. +func TestBALInEVMCreateDeploysContract(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // Factory code: + // Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0. + // Then CREATE(value=0, offset=0, length=5). + // + // Layout: store the init code as a single 32-byte word at offset 0 via MSTORE + // with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5. + initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + var word [32]byte + copy(word[32-len(initBlob):], initBlob) + code := []byte{0x7f} // PUSH32 + code = append(code, word[:]...) + code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE + // CREATE expects [value, offset, length] with value on bottom of stack. + code = append(code, + 0x60, 0x05, // PUSH1 5 (length) + 0x60, 0x1b, // PUSH1 27 (offset) + 0x60, 0x00, // PUSH1 0 (value) + 0xf0, // CREATE + 0x00, // STOP (discard result) + ) + + env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil)) + }) + + // Deployed address depends on the factory's nonce at the moment of CREATE, + // which is the factory's genesis nonce (1). + deployed := crypto.CreateAddress(factory, 1) + aa := assertPresent(t, b, deployed) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges) + } +} + +// ============================== SELFDESTRUCT ============================== + +// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh +// beneficiary when the destructing account has 0 balance — both addresses are +// listed with empty change sets (no balance entry). +func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code performs SELFDESTRUCT to beneficiary inside the constructor, + // so EIP-6780's same-tx requirement is satisfied. The destructing account + // starts with balance 0 because the creation tx sends 0 value. + // PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 0 { + t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges) + } + cc := assertPresent(t, b, created) + if len(cc.BalanceChanges) != 0 { + t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges) + } +} + +// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly +// created contract that received positive value — beneficiary records the +// credit; destructing account's balance entry is omitted because its +// pre-transaction balance was 0. +func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code: PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init)) + }) + + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 { + t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges) + } +} + +// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing +// contract with positive balance records balance→0 for the contract and the +// corresponding credit on the beneficiary. EIP-6780 means the contract is +// only credited and not deleted, but its balance moves regardless. +func TestBALSelfDestructPreExistingContract(t *testing.T) { + suicidal := common.HexToAddress("0x5e1f") + beneficiary := common.HexToAddress("0xbeefbeef") + // PUSH20 SELFDESTRUCT + code := append([]byte{0x73}, beneficiary.Bytes()...) + code = append(code, 0xff) + + env := newBALTestEnv(types.GenesisAlloc{ + suicidal: {Code: code, Balance: big.NewInt(50)}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, suicidal) + if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() { + t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges) + } + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 { + t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges) + } +} + +// ============================== Mid-tx balance round-trip ============================== + +// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a +// transaction but returns to its pre-transaction value, the address is still +// listed in the BAL but MUST NOT have a balance entry. +func TestBALMidTxBalanceRoundTrip(t *testing.T) { + bouncer := common.HexToAddress("0xb0unce") + // On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE + // and zero data. Net effect: bouncer.balance returns to its pre-tx value. + // + // PUSH1 0 (retSize) + // PUSH1 0 (retOff) + // PUSH1 0 (argsSize) + // PUSH1 0 (argsOff) + // CALLVALUE + // CALLER + // GAS + // CALL + // POP + // STOP + code := []byte{ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, + 0x34, // CALLVALUE + 0x33, // CALLER + 0x5a, // GAS + 0xf1, // CALL + 0x50, // POP + 0x00, // STOP + } + env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, bouncer) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges) + } +} + +// ============================== System contracts (pre/post-execution) ============================== + +// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses +// accessed during pre/post-execution" MUST be included in the BAL. That +// means all four of the post-merge system contracts touched by every +// Amsterdam block: +// +// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set) +// - EIP-2935 history storage (pre-execution) +// - EIP-7002 withdrawal queue (post-execution) +// - EIP-7251 consolidation queue (post-execution) +func TestBALSystemContractsPresent(t *testing.T) { + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788. + g.SetCoinbase(common.Address{0xc0}) + g.SetParentBeaconRoot(common.Hash{0xbe, 0xac}) + }) + + for _, sys := range []struct { + name string + addr common.Address + }{ + {"BeaconRoots (4788)", params.BeaconRootsAddress}, + {"HistoryStorage (2935)", params.HistoryStorageAddress}, + {"WithdrawalQueue (7002)", params.WithdrawalQueueAddress}, + {"ConsolidationQueue (7251)", params.ConsolidationQueueAddress}, + } { + if findAccount(b, sys.addr) == nil { + t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint()) + } + } +} + +// ============================== Withdrawals ============================== + +// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts +// the recipient in the BAL (with no balance entry). +func TestBALWithdrawalZeroAmountIncluded(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges) + } +} + +// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal +// records a balance change for the recipient. +func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges) + } +} + +// ============================== EIP-7702 authority ============================== + +// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code +// transaction is added to the BAL once its delegation is loaded, recording +// both the nonce bump and the delegation-pointer code entry. +func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) == 0 { + t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) == 0 { + t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges) + } +} + +// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target +// MUST NOT appear in the BAL when only the authorization is installed and the +// target is never loaded as an execution target. +func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + delegate := common.HexToAddress("0xdeadbeef") // never accessed + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, // tx.to is an EOA with no code: delegate is never called + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, delegate) +} + +// newSetCodeTx is a small constructor used by the multi-auth tests below. +func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction { + t.Helper() + tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{ + ChainID: uint256.MustFromBig(e.cfg.ChainID), + Nonce: nonce, + To: to, + Value: new(uint256.Int), + Gas: 400_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: auths, + }), e.signer, e.key) + if err != nil { + t.Fatalf("sign SetCodeTx: %v", err) + } + return tx +} + +// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check +// fails returns before the authority is loaded, so the authority address +// MUST NOT appear in the BAL. +func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load) + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + assertAbsent(t, b, authority) +} + +// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the +// nonce check happens AFTER the authority's code is loaded (and the address +// added to accessed_addresses), so the authority MUST appear in the BAL — +// but with no nonce or code change. +func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + // The authority's actual nonce is 0; supplying auth.Nonce=99 makes + // validation fail only after the code has been loaded. + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 99, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 0 { + t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and +// pre-load-failed auths lists only the loaded authorities in the BAL. +func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) { + env := newBALTestEnv(nil) + goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030") + good := crypto.PubkeyToAddress(goodKey.PublicKey) + bad := crypto.PubkeyToAddress(badKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign good auth: %v", err) + } + badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // fails before load + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign bad auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth})) + }) + + assertPresent(t, b, good) // loaded → in BAL + assertAbsent(t, b, bad) // never loaded → not in BAL +} + +// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that +// (1) install a delegation and (2) clear it again. Final code equals pre-tx +// code (empty), so the BAL records only the cumulative nonce bump and NO +// code change. +func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, // empty → A + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.Address{}, // delegation to zero clears the code (A → empty) + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority +// switching delegation A → B record exactly one code change carrying the +// final delegation pointer (B), not the intermediate value. +func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + delegateB := common.HexToAddress("0xb0b0b0") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateB, + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.CodeChanges) != 1 { + t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges) + } + want := types.AddressToDelegation(delegateB) + if !bytes.Equal(aa.CodeChanges[0].Code, want) { + t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code) + } + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } +} diff --git a/core/bench_test.go b/core/bench_test.go index 20d1a7794b..65179c54d4 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false) + cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 1b033151d3..b49ac83bb5 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -63,12 +64,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain @@ -92,6 +93,7 @@ func TestProcessUBT(t *testing.T) { // genesis := gspec.MustCommit(bcdb, triedb) options := DefaultConfig().WithStateScheme(rawdb.PathScheme) options.SnapshotLimit = 0 + options.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth blockchain, _ := NewBlockChain(bcdb, gspec, beacon.New(ethash.NewFaker()), options) defer blockchain.Stop() @@ -201,7 +203,7 @@ func TestProcessParentBlockHash(t *testing.T) { } vmContext := NewEVMBlockContext(header, nil, new(common.Address)) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{}) - ProcessParentBlockHash(header.ParentHash, evm) + ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) } // Read block hashes for block 0 .. num-1 for i := 0; i < num; i++ { @@ -218,6 +220,7 @@ func TestProcessParentBlockHash(t *testing.T) { t.Run("UBT", func(t *testing.T) { db := rawdb.NewMemoryDatabase() cacheConfig := DefaultConfig().WithStateScheme(rawdb.PathScheme) + cacheConfig.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth cacheConfig.SnapshotLimit = 0 triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig(true)) statedb, _ := state.New(types.EmptyBinaryHash, state.NewDatabase(triedb, nil)) diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..4086a2ead7 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,28 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // Block access list hash must be present in header after the + // Amsterdam hard fork. + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + // If the block does not include an access list, compute it locally during + // execution and validate it against the access list hash in the header. + // + // If the block includes an attached access list, validate it directly here. + if block.AccessList() != nil { + computed := block.AccessList().Hash() + if *block.Header().BlockAccessListHash != computed { + return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash) + } else if err := block.AccessList().Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } + } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil { + return errors.New("block had access list before Amsterdam") + } + // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { @@ -160,6 +182,23 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD } else if res.Requests != nil { return errors.New("block has requests before prague fork") } + // Verify Block-level accessList once Amsterdam is enabled + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if res.Bal == nil { + return errors.New("block access list is not available in amsterdam") + } + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + enc := res.Bal.ToEncodingObj() + local, remote := enc.Hash(), *block.Header().BlockAccessListHash + if local != remote { + return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote) + } + if err := enc.Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } // Validate the state root against the received state root and throw // an error if they don't match. if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { diff --git a/core/blockchain.go b/core/blockchain.go index 296ef6bc16..7b5a910b7a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -170,9 +170,10 @@ type BlockChainConfig struct { TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed TrieJournalDirectory string // Directory path to the journal used for persisting trie data across node restarts - Preimages bool // Whether to store preimage of trie key to the disk - StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top - ArchiveMode bool // Whether to enable the archive mode + Preimages bool // Whether to store preimage of trie key to the disk + StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top + ArchiveMode bool // Whether to enable the archive mode + BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8) // Number of blocks from the chain head for which state histories are retained. // If set to 0, all state histories across the entire chain will be retained; @@ -260,8 +261,9 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig { // triedbConfig derives the configures for trie database. func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config { config := &triedb.Config{ - Preimages: cfg.Preimages, - IsUBT: isUBT, + Preimages: cfg.Preimages, + IsUBT: isUBT, + BinTrieGroupDepth: cfg.BinTrieGroupDepth, } if cfg.StateScheme == rawdb.HashScheme { config.HashDB = &hashdb.Config{ @@ -1184,6 +1186,7 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error { } // If all checks out, manually set the head block. + rawdb.WriteHeadBlockHash(bc.db, hash) bc.currentBlock.Store(block.Header()) headBlockGauge.Update(int64(block.NumberU64())) @@ -2594,8 +2597,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error blockReorgAddMeter.Mark(int64(len(newChain))) } else { // len(newChain) == 0 && len(oldChain) > 0 - // rewind the canonical chain to a lower point. - log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain), "newnum", newHead.Number, "newhash", newHead.Hash(), "newblocks", len(newChain)) + // Rewind the canonical chain to a lower point. In EPBs we can reorg to + // a parent of the head within 32 blocks. + if len(oldChain) > 32 { + log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain)) + } else { + log.Info("Shorten chain", "del", len(oldChain), "number", oldHead.Number, "hash", oldHead.Hash()) + } } // Acquire the tx-lookup lock before mutation. This step is essential // as the txlookups should be changed atomically, and all subsequent diff --git a/core/chain_makers.go b/core/chain_makers.go index 46cd98de61..2e856b5161 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -17,6 +17,7 @@ package core import ( + "context" "fmt" "math/big" @@ -28,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/params" @@ -49,6 +51,7 @@ type BlockGen struct { receipts []*types.Receipt uncles []*types.Header withdrawals []*types.Withdrawal + bal *bal.ConstructionBlockAccessList engine consensus.Engine } @@ -98,7 +101,7 @@ func (b *BlockGen) Difficulty() *big.Int { func (b *BlockGen) SetParentBeaconRoot(root common.Hash) { b.header.ParentBeaconRoot = &root blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) - ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{})) + ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}), b.bal) } // addTx adds a transaction to the generated block. If no coinbase has @@ -116,8 +119,8 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase) evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) - b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) + b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1)) + receipt, bal, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } @@ -133,6 +136,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti if b.header.BlobGasUsed != nil { *b.header.BlobGasUsed += receipt.BlobGasUsed } + b.bal.Merge(bal) } // AddTx adds a transaction to the generated block. If no coinbase has @@ -303,10 +307,11 @@ func (b *BlockGen) OffsetTime(seconds int64) { // ConsensusLayerRequests returns the EIP-7685 requests which have accumulated so far. func (b *BlockGen) ConsensusLayerRequests() [][]byte { - return b.collectRequests(true) + requests, _ := b.collectRequests(true) + return requests } -func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { +func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.ConstructionBlockAccessList) { statedb := b.statedb if readonly { // The system contracts clear themselves on a system-initiated read. @@ -314,30 +319,19 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { // off the statedb before executing the system calls. statedb = statedb.Copy() } - - if b.cm.config.IsPrague(b.header.Number, b.header.Time) { - requests = [][]byte{} - // EIP-6110 deposits - var blockLogs []*types.Log - for _, r := range b.receipts { - blockLogs = append(blockLogs, r.Logs...) - } - if err := ParseDepositLogs(&requests, blockLogs, b.cm.config); err != nil { - panic(fmt.Sprintf("failed to parse deposit log: %v", err)) - } - // create EVM for system calls - blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) - evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { - panic(fmt.Sprintf("could not process withdrawal requests: %v", err)) - } - // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { - panic(fmt.Sprintf("could not process consolidation requests: %v", err)) - } + var blockLogs []*types.Log + for _, r := range b.receipts { + blockLogs = append(blockLogs, r.Logs...) } - return requests + // TODO use the shared EVM throughout the entire generation cycle + blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) + evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) + + requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) + if err != nil { + panic(fmt.Sprintf("failed to run post-execution: %v", err)) + } + return requests, bal } // GenerateChain creates a chain of n blocks. The first block's @@ -364,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) { b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine} b.header = cm.makeHeader(parent, statedb, b.engine) + b.bal = bal.NewConstructionBlockAccessList() // Set the difficulty for clique block. The chain maker doesn't have access // to a chain, so the difficulty will be left unset (nil). Set it here to the @@ -396,7 +391,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase) blockContext.Random = &common.Hash{} // enable post-merge instruction set evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{}) - ProcessParentBlockHash(b.header.ParentHash, evm) + ProcessParentBlockHash(b.header.ParentHash, evm, b.bal) } // Execute any user modifications to the block @@ -404,11 +399,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse gen(i, b) } - requests := b.collectRequests(false) + requests, bal := b.collectRequests(false) if requests != nil { reqHash := types.CalcRequestsHash(requests) b.header.RequestsHash = &reqHash } + b.bal.Merge(bal) body := types.Body{ Transactions: b.txs, @@ -424,8 +420,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse body.Withdrawals = make([]*types.Withdrawal, 0) } } + // Apply the consensus-specific post-transaction changes + b.engine.Finalize(cm, b.header, statedb, &body, uint32(len(b.txs)+1), b.bal) + // Assemble the block for delivery. - block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts) + block := AssembleBlock(cm, b.header, statedb, &body, b.receipts, b.bal) // Write state changes to db root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number), config.IsCancun(b.header.Number, b.header.Time)) diff --git a/core/evm.go b/core/evm.go index 818b23bee5..73e4c01a99 100644 --- a/core/evm.go +++ b/core/evm.go @@ -87,7 +87,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common func NewEVMTxContext(msg *Message) vm.TxContext { ctx := vm.TxContext{ Origin: msg.From, - GasPrice: uint256.MustFromBig(msg.GasPrice), + GasPrice: msg.GasPrice, BlobHashes: msg.BlobHashes, } return ctx diff --git a/core/genesis.go b/core/genesis.go index d77ea10d8c..e1c67e57c2 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -136,8 +136,9 @@ func hashAlloc(ga *types.GenesisAlloc, isUBT bool) (common.Hash, error) { var config *triedb.Config if isUBT { config = &triedb.Config{ - PathDB: pathdb.Defaults, - IsUBT: true, + PathDB: pathdb.Defaults, + IsUBT: true, + BinTrieGroupDepth: triedb.UBTDefaults.BinTrieGroupDepth, } } // Create an ephemeral in-memory database for computing hash, @@ -554,6 +555,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if head.SlotNumber == nil { head.SlotNumber = new(uint64) } + head.BlockAccessListHash = &types.EmptyBlockAccessListHash } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) diff --git a/core/genesis_test.go b/core/genesis_test.go index e15ad00222..94f1b3a4fd 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -261,9 +261,9 @@ func newDbConfig(scheme string) *triedb.Config { return &triedb.Config{PathDB: &config} } -func TestVerkleGenesisCommit(t *testing.T) { - var verkleTime uint64 = 0 - verkleConfig := ¶ms.ChainConfig{ +func TestBinaryGenesisCommit(t *testing.T) { + var ubtTime uint64 = 0 + ubtConfig := ¶ms.ChainConfig{ ChainID: big.NewInt(1), HomesteadBlock: big.NewInt(0), DAOForkBlock: nil, @@ -281,11 +281,11 @@ func TestVerkleGenesisCommit(t *testing.T) { ArrowGlacierBlock: big.NewInt(0), GrayGlacierBlock: big.NewInt(0), MergeNetsplitBlock: nil, - ShanghaiTime: &verkleTime, - CancunTime: &verkleTime, - PragueTime: &verkleTime, - OsakaTime: &verkleTime, - UBTTime: &verkleTime, + ShanghaiTime: &ubtTime, + CancunTime: &ubtTime, + PragueTime: &ubtTime, + OsakaTime: &ubtTime, + UBTTime: &ubtTime, TerminalTotalDifficulty: big.NewInt(0), EnableUBTAtGenesis: true, Ethash: nil, @@ -300,8 +300,8 @@ func TestVerkleGenesisCommit(t *testing.T) { genesis := &Genesis{ BaseFee: big.NewInt(params.InitialBaseFee), - Config: verkleConfig, - Timestamp: verkleTime, + Config: ubtConfig, + Timestamp: ubtTime, Difficulty: big.NewInt(0), Alloc: types.GenesisAlloc{ {1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}}, @@ -320,17 +320,18 @@ func TestVerkleGenesisCommit(t *testing.T) { config.NoAsyncFlush = true triedb := triedb.NewDatabase(db, &triedb.Config{ - IsUBT: true, - PathDB: &config, + IsUBT: true, + PathDB: &config, + BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth, }) block := genesis.MustCommit(db, triedb) if !bytes.Equal(block.Root().Bytes(), expected) { t.Fatalf("invalid genesis state root, expected %x, got %x", expected, block.Root()) } - // Test that the trie is verkle + // Test that the trie is a unified binary trie if !triedb.IsUBT() { - t.Fatalf("expected trie to be verkle") + t.Fatalf("expected trie to be a unified binary trie") } vdb := rawdb.NewTable(db, string(rawdb.VerklePrefix)) if !rawdb.HasAccountTrieNode(vdb, nil) { diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index 718d93df87..16579f6d6a 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -96,7 +96,7 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea // OpenTrie opens the main account trie at a specific root hash. func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) { - return bintrie.NewBinaryTrie(root, db.triedb) + return bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth()) } // OpenStorageTrie opens the storage trie of an account. In binary trie mode, diff --git a/core/state/journal.go b/core/state/journal.go index a79bd7331a..353144a1c7 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -18,7 +18,6 @@ package state import ( "fmt" - "maps" "slices" "sort" @@ -32,26 +31,163 @@ type revision struct { journalIndex int } +// journalMutationKind indicates the type of account mutation. +type journalMutationKind uint8 + +const ( + // journalMutationKindNone is the zero value returned by mutation() for + // entries that don't carry a tracked account mutation. The accompanying + // bool is false in that case; callers must gate on it before using the + // kind. + journalMutationKindNone journalMutationKind = iota + journalMutationKindTouch + journalMutationKindCreate + journalMutationKindSelfDestruct + journalMutationKindBalance + journalMutationKindNonce + journalMutationKindCode + journalMutationKindStorage + journalMutationKindCount // sentinel, must stay last +) + +type journalMutationCounts [journalMutationKindCount]int + +// journalMutationState tracks, per account, both the per-kind count of mutation +// entries currently present in the journal and the pre-tx value of each +// metadata field captured on its first touch (balance/nonce/code). +// The *Set flags indicate whether the corresponding field has been mutated +// at least once in the current tx window; they are cleared when all entries +// of that kind are reverted. Storage slots are tracked elsewhere. +type journalMutationState struct { + counts journalMutationCounts + + balance *uint256.Int + balanceSet bool + nonce uint64 + nonceSet bool + code []byte + codeSet bool +} + +func (s *journalMutationState) add(kind journalMutationKind) { + s.counts.add(kind) +} + +// remove drops one occurrence of the given mutation kind. It returns a flag +// indicating whether no entries of any kind remain. +func (s *journalMutationState) remove(kind journalMutationKind) bool { + if s.counts.remove(kind) { + // No entries of this kind remain for this account; drop the + // corresponding stashed original so the state mirrors the + // live mutation set. + s.clearKind(kind) + } + return s.counts == (journalMutationCounts{}) +} + +// clearKind drops the stashed original for the given mutation kind. It is +// invoked during revert once no journal entries of that kind remain for the +// account. Kinds that don't correspond to a tracked metadata field are no-ops. +func (s *journalMutationState) clearKind(kind journalMutationKind) { + switch kind { + case journalMutationKindBalance: + s.balance = nil + s.balanceSet = false + case journalMutationKindNonce: + s.nonce = 0 + s.nonceSet = false + case journalMutationKindCode: + s.code = nil + s.codeSet = false + } +} + +func (s *journalMutationState) copy() *journalMutationState { + cpy := *s + if s.balance != nil { + cpy.balance = new(uint256.Int).Set(s.balance) + } + if s.code != nil { + cpy.code = slices.Clone(s.code) + } + return &cpy +} + +func (c *journalMutationCounts) add(kind journalMutationKind) { + c[kind]++ +} + +func (c *journalMutationCounts) remove(kind journalMutationKind) bool { + c[kind]-- + return c[kind] == 0 +} + // journalEntry is a modification entry in the state change journal that can be // reverted on demand. type journalEntry interface { // revert undoes the changes introduced by this journal entry. revert(*StateDB) - // dirtied returns the Ethereum address modified by this journal entry. - // indicates false if no address was changed. - dirtied() (common.Address, bool) + // mutation returns the account mutation introduced by this entry. + // It indicates false if no tracked account mutation was made. + mutation() (common.Address, journalMutationKind, bool) // copy returns a deep-copied journal entry. copy() journalEntry } +// stashBalance records prev as the pre-tx balance of addr, iff this is the +// first balance touch seen in the current tx. Subsequent balance writes are +// ignored so the stored value remains the true pre-tx original. +func (j *journal) stashBalance(addr common.Address, prev *uint256.Int) { + s := j.mutationStateFor(addr) + if s.balanceSet { + return + } + // The balance is already deep-copied and safe to hold the object here. + s.balance = prev + s.balanceSet = true +} + +// stashNonce records prev as the pre-tx nonce of addr on first touch. +func (j *journal) stashNonce(addr common.Address, prev uint64) { + s := j.mutationStateFor(addr) + if s.nonceSet { + return + } + s.nonce = prev + s.nonceSet = true +} + +// stashCode records prev as the pre-tx code of addr on first touch. +func (j *journal) stashCode(addr common.Address, prev []byte) { + s := j.mutationStateFor(addr) + if s.codeSet { + return + } + // The code is already deep-copied in the StateDB, safe to + // hold the reference here. + s.code = prev + s.codeSet = true +} + +// mutationStateFor returns the mutation state for addr, creating an empty one +// if absent. +func (j *journal) mutationStateFor(addr common.Address) *journalMutationState { + s := j.mutations[addr] + if s == nil { + s = new(journalMutationState) + j.mutations[addr] = s + } + return s +} + // journal contains the list of state modifications applied since the last state // commit. These are tracked to be able to be reverted in the case of an execution // exception or request for reversal. type journal struct { - entries []journalEntry // Current changes tracked by the journal - dirties map[common.Address]int // Dirty accounts and the number of changes + entries []journalEntry // Current changes tracked by the journal + mutations map[common.Address]*journalMutationState // Per-account mutation kinds and pre-tx originals validRevisions []revision nextRevisionId int @@ -60,7 +196,7 @@ type journal struct { // newJournal creates a new initialized journal. func newJournal() *journal { return &journal{ - dirties: make(map[common.Address]int), + mutations: make(map[common.Address]*journalMutationState), } } @@ -70,7 +206,7 @@ func newJournal() *journal { func (j *journal) reset() { j.entries = j.entries[:0] j.validRevisions = j.validRevisions[:0] - clear(j.dirties) + clear(j.mutations) j.nextRevisionId = 0 } @@ -101,33 +237,52 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) { // append inserts a new modification entry to the end of the change journal. func (j *journal) append(entry journalEntry) { j.entries = append(j.entries, entry) - if addr, dirty := entry.dirtied(); dirty { - j.dirties[addr]++ + if addr, kind, dirty := entry.mutation(); dirty { + state := j.mutations[addr] + if state == nil { + state = new(journalMutationState) + j.mutations[addr] = state + } + state.add(kind) } } // revert undoes a batch of journalled modifications along with any reverted -// dirty handling too. +// mutation tracking too. func (j *journal) revert(statedb *StateDB, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // Undo the changes made by the operation j.entries[i].revert(statedb) - // Drop any dirty tracking induced by the change - if addr, dirty := j.entries[i].dirtied(); dirty { - if j.dirties[addr]--; j.dirties[addr] == 0 { - delete(j.dirties, addr) + // Drop any mutation tracking induced by the change. + if addr, kind, dirty := j.entries[i].mutation(); dirty { + state := j.mutations[addr] + if state == nil { + panic(fmt.Errorf("journal mutation tracking missing for %x", addr[:])) + } + if state.remove(kind) { + delete(j.mutations, addr) } } } j.entries = j.entries[:snapshot] } -// dirty explicitly sets an address to dirty, even if the change entries would -// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD -// precompile consensus exception. -func (j *journal) dirty(addr common.Address) { - j.dirties[addr]++ +// ripemdMagic explicitly keeps RIPEMD160 in the mutation set with a touch change. +// +// Ethereum Mainnet contains an old empty-account touch/revert quirk for address +// 0x03. If we only relied on the journal entry above, the revert path would +// remove the account from the mutation set together with the touch. +// +// Keep an explicit touch marker so tx finalisation still sees RIPEMD160 +// on the mutation pass when replaying that historical case. +func (j *journal) ripemdMagic() { + state := j.mutations[ripemd] + if state == nil { + state = new(journalMutationState) + j.mutations[ripemd] = state + } + state.add(journalMutationKindTouch) } // length returns the current number of entries in the journal. @@ -141,9 +296,13 @@ func (j *journal) copy() *journal { for i := 0; i < j.length(); i++ { entries = append(entries, j.entries[i].copy()) } + mutations := make(map[common.Address]*journalMutationState, len(j.mutations)) + for addr, state := range j.mutations { + mutations[addr] = state.copy() + } return &journal{ entries: entries, - dirties: maps.Clone(j.dirties), + mutations: mutations, validRevisions: slices.Clone(j.validRevisions), nextRevisionId: j.nextRevisionId, } @@ -187,13 +346,16 @@ func (j *journal) refundChange(previous uint64) { } func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) { + prev := previous.Clone() + j.stashBalance(addr, prev) j.append(balanceChange{ account: addr, - prev: previous.Clone(), + prev: prev, }) } func (j *journal) setCode(address common.Address, prevCode []byte) { + j.stashCode(address, prevCode) j.append(codeChange{ account: address, prevCode: prevCode, @@ -201,6 +363,7 @@ func (j *journal) setCode(address common.Address, prevCode []byte) { } func (j *journal) nonceChange(address common.Address, prev uint64) { + j.stashNonce(address, prev) j.append(nonceChange{ account: address, prev: prev, @@ -212,9 +375,18 @@ func (j *journal) touchChange(address common.Address) { account: address, }) if address == ripemd { - // Explicitly put it in the dirty-cache, which is otherwise generated from - // flattened journals. - j.dirty(address) + // Preserve the historical RIPEMD160 precompile consensus exception. + // + // Mainnet contains an old empty-account touch/revert quirk for address + // 0x03. If we only relied on the journal entry above, the revert path + // would remove the account from the dirty set together with the touch. + // Keep an explicit dirty marker so tx finalisation still sees the + // account on the dirty pass when replaying that historical case. + // + // This does not force deletion by itself: Finalise will still delete the + // account only if the state object is present at tx end and qualifies for + // deletion there. + j.ripemdMagic() } } @@ -295,8 +467,8 @@ func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, ch.account) } -func (ch createObjectChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch createObjectChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindCreate, true } func (ch createObjectChange) copy() journalEntry { @@ -309,8 +481,8 @@ func (ch createContractChange) revert(s *StateDB) { s.getStateObject(ch.account).newContract = false } -func (ch createContractChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch createContractChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch createContractChange) copy() journalEntry { @@ -326,8 +498,8 @@ func (ch selfDestructChange) revert(s *StateDB) { } } -func (ch selfDestructChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch selfDestructChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindSelfDestruct, true } func (ch selfDestructChange) copy() journalEntry { @@ -341,8 +513,8 @@ var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") func (ch touchChange) revert(s *StateDB) { } -func (ch touchChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch touchChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindTouch, true } func (ch touchChange) copy() journalEntry { @@ -355,8 +527,8 @@ func (ch balanceChange) revert(s *StateDB) { s.getStateObject(ch.account).setBalance(ch.prev) } -func (ch balanceChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch balanceChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindBalance, true } func (ch balanceChange) copy() journalEntry { @@ -370,8 +542,8 @@ func (ch nonceChange) revert(s *StateDB) { s.getStateObject(ch.account).setNonce(ch.prev) } -func (ch nonceChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch nonceChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindNonce, true } func (ch nonceChange) copy() journalEntry { @@ -385,8 +557,8 @@ func (ch codeChange) revert(s *StateDB) { s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode) } -func (ch codeChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch codeChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindCode, true } func (ch codeChange) copy() journalEntry { @@ -400,8 +572,8 @@ func (ch storageChange) revert(s *StateDB) { s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue) } -func (ch storageChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch storageChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindStorage, true } func (ch storageChange) copy() journalEntry { @@ -417,8 +589,8 @@ func (ch transientStorageChange) revert(s *StateDB) { s.setTransientState(ch.account, ch.key, ch.prevalue) } -func (ch transientStorageChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch transientStorageChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch transientStorageChange) copy() journalEntry { @@ -433,8 +605,8 @@ func (ch refundChange) revert(s *StateDB) { s.refund = ch.prev } -func (ch refundChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch refundChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch refundChange) copy() journalEntry { @@ -453,8 +625,8 @@ func (ch addLogChange) revert(s *StateDB) { s.logSize-- } -func (ch addLogChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch addLogChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch addLogChange) copy() journalEntry { @@ -476,8 +648,8 @@ func (ch accessListAddAccountChange) revert(s *StateDB) { s.accessList.DeleteAddress(ch.address) } -func (ch accessListAddAccountChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch accessListAddAccountChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch accessListAddAccountChange) copy() journalEntry { @@ -490,8 +662,8 @@ func (ch accessListAddSlotChange) revert(s *StateDB) { s.accessList.DeleteSlot(ch.address, ch.slot) } -func (ch accessListAddSlotChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch accessListAddSlotChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch accessListAddSlotChange) copy() journalEntry { diff --git a/core/state/journal_test.go b/core/state/journal_test.go new file mode 100644 index 0000000000..262cee77fe --- /dev/null +++ b/core/state/journal_test.go @@ -0,0 +1,219 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" +) + +// fuzzJournalAddrs is a small fixed pool used by the fuzz harness to force +// repeated collisions on the same account, which exercises the multi-entry +// path in the journal's mutation tracking and originals cleanup on revert. +// It deliberately excludes the RIPEMD-160 precompile (0x03), which has a +// consensus-level touch/revert exception that would complicate invariants. +var fuzzJournalAddrs = []common.Address{ + common.BytesToAddress([]byte{0x11}), + common.BytesToAddress([]byte{0x22}), + common.BytesToAddress([]byte{0x44}), +} + +// checkJournalInvariants validates that: +// - journal.mutations exactly reflects the dirty entries currently in +// journal.entries (per-kind counts and mask match what you'd get by +// walking the entries from scratch). +// - journal.originals mirrors that set for the three tracked metadata kinds +// (balance/nonce/code): a *Set flag is true iff the account currently has +// at least one corresponding entry in the journal. +// - An address is present in originals only if it also has at least one +// tracked-kind mutation in the journal. +func checkJournalInvariants(t *testing.T, j *journal) { + t.Helper() + + // Reconstruct the expected per-address counts from the live entries. + expected := make(map[common.Address]*journalMutationCounts) + for _, e := range j.entries { + addr, kind, dirty := e.mutation() + if !dirty { + continue + } + c := expected[addr] + if c == nil { + c = &journalMutationCounts{} + expected[addr] = c + } + c.add(kind) + } + + if len(j.mutations) != len(expected) { + t.Fatalf("mutations size %d, want %d", len(j.mutations), len(expected)) + } + for addr, state := range j.mutations { + want, ok := expected[addr] + if !ok { + t.Fatalf("mutations has extra address %x", addr) + } + if state.counts != *want { + t.Fatalf("addr %x: counts=%+v want=%+v", addr, state.counts, *want) + } + // First-touch *Set flags must mirror the live per-kind counts. + if state.balanceSet != (want[journalMutationKindBalance] > 0) { + t.Fatalf("addr %x: balanceSet=%v want=%v (balance count=%d)", + addr, state.balanceSet, want[journalMutationKindBalance] > 0, want[journalMutationKindBalance]) + } + if state.nonceSet != (want[journalMutationKindNonce] > 0) { + t.Fatalf("addr %x: nonceSet=%v want=%v (nonce count=%d)", + addr, state.nonceSet, want[journalMutationKindNonce] > 0, want[journalMutationKindNonce]) + } + if state.codeSet != (want[journalMutationKindCode] > 0) { + t.Fatalf("addr %x: codeSet=%v want=%v (code count=%d)", + addr, state.codeSet, want[journalMutationKindCode] > 0, want[journalMutationKindCode]) + } + } +} + +// FuzzJournal drives a randomised sequence of state mutations, snapshots and +// reverts against a fresh StateDB and validates the journal's internal +// bookkeeping invariants after every step. It also asserts that reverting +// back to the root snapshot empties mutations, originals and entries +// completely. The seed corpus ensures the test also runs as a regular unit +// test via `go test -run FuzzJournal`. +func FuzzJournal(f *testing.F) { + seeds := [][]byte{ + // balance then full revert (simplest a→b→a case). + {0x00, 0x00, 0x05, 0x05, 0x00}, + // balance+nonce+code mixed, then revert to root. + {0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x05, 0x00}, + // snapshot, mutate, revert, mutate again. + {0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x01, 0x05}, + // storage interleaved with metadata. + {0x03, 0x00, 0x01, 0x00, 0x01, 0x05, 0x03, 0x02, 0x02, 0x04, 0x03, 0x01, 0x07}, + // many ops, no explicit revert — exercises steady-state invariants. + {0x00, 0x01, 0x02, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x00, 0x01, 0x02, 0x00, 0x06, 0x08, 0x0a, 0x0c}, + } + for _, s := range seeds { + f.Add(s) + } + + f.Fuzz(func(t *testing.T, data []byte) { + sdb, err := New(types.EmptyRootHash, NewDatabaseForTesting()) + if err != nil { + t.Fatal(err) + } + root := sdb.Snapshot() + + // Stack of snapshot IDs taken during the fuzz loop. + var pending []int + + // readByte returns the next byte and advances the cursor. Returns + // (0, false) if exhausted. + i := 0 + readByte := func() (byte, bool) { + if i >= len(data) { + return 0, false + } + b := data[i] + i++ + return b, true + } + + for { + op, ok := readByte() + if !ok { + break + } + switch op % 6 { + case 0: // SetBalance + a, ok1 := readByte() + v, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetBalance(addr, uint256.NewInt(uint64(v)), tracing.BalanceChangeUnspecified) + case 1: // SetNonce + a, ok1 := readByte() + n, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetNonce(addr, uint64(n), tracing.NonceChangeUnspecified) + case 2: // SetCode + a, ok1 := readByte() + l, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + code := make([]byte, int(l)%8) + for k := range code { + b, ok := readByte() + if !ok { + break + } + code[k] = b + } + sdb.SetCode(addr, code, tracing.CodeChangeUnspecified) + case 3: // SetState (storage; tracked as mutation kind, no original) + a, ok1 := readByte() + k, ok2 := readByte() + v, ok3 := readByte() + if !ok1 || !ok2 || !ok3 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetState(addr, + common.BytesToHash([]byte{k}), + common.BytesToHash([]byte{v})) + case 4: // Snapshot + pending = append(pending, sdb.Snapshot()) + case 5: // RevertToSnapshot + if len(pending) == 0 { + break + } + sel, ok := readByte() + if !ok { + break + } + idx := int(sel) % len(pending) + sdb.RevertToSnapshot(pending[idx]) + pending = pending[:idx] + } + checkJournalInvariants(t, sdb.journal) + } + + // After reverting to the root snapshot, the journal must be fully + // drained: no entries, no mutations, no originals. This is the core + // guarantee the user cares about — "all mutations against a single + // account reverted" taken to its limit across every account. + sdb.RevertToSnapshot(root) + checkJournalInvariants(t, sdb.journal) + + if n := len(sdb.journal.entries); n != 0 { + t.Fatalf("entries not drained after revert-to-root: %d remain", n) + } + if n := len(sdb.journal.mutations); n != 0 { + t.Fatalf("mutations not drained after revert-to-root: %d remain", n) + } + }) +} diff --git a/core/state/reader.go b/core/state/reader.go index 5df0acbb9b..be07cec0f9 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -255,7 +255,7 @@ type ubtTrieReader struct { // newUBTTrieReader constructs a Unified-binary-trie reader of the specific state. // An error will be returned if the associated trie specified by root is not existent. func newUBTTrieReader(root common.Hash, db *triedb.Database) (*ubtTrieReader, error) { - binTrie, binErr := bintrie.NewBinaryTrie(root, db) + binTrie, binErr := bintrie.NewBinaryTrie(root, db, db.BinTrieGroupDepth()) if binErr != nil { return nil, binErr } diff --git a/core/state/snapshot/iterator_test.go b/core/state/snapshot/iterator_test.go index a95bd66dde..8e473aa312 100644 --- a/core/state/snapshot/iterator_test.go +++ b/core/state/snapshot/iterator_test.go @@ -441,7 +441,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) { if i%8 == 0 { e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) } - if i > 50 || i < 85 { + if i > 50 && i < 85 { f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) } if i%64 == 0 { diff --git a/core/state/state_object.go b/core/state/state_object.go index 8e72486825..ce456e7668 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -184,8 +184,9 @@ func (s *stateObject) getState(key common.Hash) (common.Hash, common.Hash) { // without any mutations caused in the current execution. func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // Record slot access regardless of whether the storage slot exists. - s.db.stateReadList.AddState(s.address, key) - + if s.db.stateAccessList != nil { + s.db.stateAccessList.StorageRead(s.address, key) + } // If we have a pending write or clean cached, return that if value, pending := s.pendingStorage[key]; pending { return value @@ -274,6 +275,13 @@ func (s *stateObject) finalise() { // map as the dirty slot might have been committed already (before the // byzantium fork) and entry is necessary to modify the value back. s.pendingStorage[key] = value + + // Aggregate storage writes into the block-level access list. + // All slots in the dirtyStorage set must have post-transaction + // values that differ from their pre-transaction values. + if s.db.stateAccessList != nil { + s.db.stateAccessList.StorageWrite(s.db.blockAccessIndex, s.address, key, value) + } } if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash { if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index 1858f4758d..1c49d46020 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,7 @@ package state import ( + "bytes" "errors" "fmt" "maps" @@ -128,7 +129,10 @@ type StateDB struct { accessEvents *AccessEvents // Per-transaction state access footprint for EIP-7928 - stateReadList *bal.StateAccessList + stateAccessList *bal.ConstructionBlockAccessList + + // Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution) + blockAccessIndex uint32 // Transient storage transientStorage transientStorage @@ -589,8 +593,9 @@ func (s *StateDB) deleteStateObject(addr common.Address) { // the object is not found or was deleted in this execution context. func (s *StateDB) getStateObject(addr common.Address) *stateObject { // Record state access regardless of whether the account exists. - s.stateReadList.AddAccount(addr) - + if s.stateAccessList != nil { + s.stateAccessList.AccountRead(addr) + } // Prefer live objects if any is available if obj := s.stateObjects[addr]; obj != nil { return obj @@ -693,6 +698,7 @@ func (s *StateDB) Copy() *StateDB { refund: s.refund, thash: s.thash, txIndex: s.txIndex, + blockAccessIndex: s.blockAccessIndex, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, preimages: maps.Clone(s.preimages), @@ -716,9 +722,6 @@ func (s *StateDB) Copy() *StateDB { if s.accessEvents != nil { state.accessEvents = s.accessEvents.Copy() } - if s.stateReadList != nil { - state.stateReadList = s.stateReadList.Copy() - } // Deep copy cached state objects. for addr, obj := range s.stateObjects { state.stateObjects[addr] = obj.deepCopy(state) @@ -740,6 +743,9 @@ func (s *StateDB) Copy() *StateDB { } state.logs[hash] = cpy } + if s.stateAccessList != nil { + state.stateAccessList = s.stateAccessList.Copy() + } return state } @@ -775,7 +781,7 @@ type removedAccountWithBalance struct { // before the Finalise. func (s *StateDB) LogsForBurnAccounts() []*types.Log { var list []removedAccountWithBalance - for addr := range s.journal.dirties { + for addr := range s.journal.mutations { if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() { list = append(list, removedAccountWithBalance{ address: obj.address, @@ -799,17 +805,20 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log { // Finalise finalises the state by removing the destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. -func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { - addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties)) - for addr := range s.journal.dirties { +func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { + addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations)) + for addr, state := range s.journal.mutations { obj, exist := s.stateObjects[addr] if !exist { - // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 - // That tx goes out of gas, and although the notion of 'touched' does not exist there, the - // touch-event will still be recorded in the journal. Since ripeMD is a special snowflake, - // it will persist in the journal even though the journal is reverted. In this special circumstance, - // it may exist in `s.journal.dirties` but not in `s.stateObjects`. - // Thus, we can safely ignore it here + // RIPEMD160 (0x03) gets an extra dirty marker for a historical + // mainnet consensus exception (at block 1714175, in tx + // 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2) + // around empty-account touch/revert handling. + // + // That marker survives journal revert, so the account may remain in + // s.journal.mutations even though its state object was rolled + // back and no longer exists. In that case there is nothing to + // finalise or delete, so ignore it here. continue } if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { @@ -822,7 +831,43 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { if _, ok := s.stateObjectsDestruct[obj.address]; !ok { s.stateObjectsDestruct[obj.address] = obj } + // Aggregate the account mutation into the block-level accessList + // if Amsterdam has been activated. + if s.stateAccessList != nil { + // Notably, if the account is deleted during the transaction, + // its pre-transaction nonce, code, and storage must be empty. + // + // EIP-6780 restricts self-destruct to contracts deployed within + // the same transaction, while EIP-7610 rejects deployments to + // destinations with non-empty storage, non-zero nonce and non-empty + // code. + // + // Therefore, when an account is deleted, its pre-transaction nonce + // code and storage is guaranteed to be empty, leaving nothing to + // clean up here. + balance := uint256.NewInt(0) + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + } } else { + // Aggregate the account mutation into the block-level accessList + // if Amsterdam has been activated. + if s.stateAccessList != nil { + balance := obj.Balance() + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + nonce := obj.Nonce() + if state.nonceSet && nonce != state.nonce { + s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce) + } + if state.codeSet { + if code := obj.Code(); !bytes.Equal(code, state.code) { + s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code) + } + } + } obj.finalise() s.markUpdate(addr) } @@ -839,7 +884,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { // Invalidate journal because reverting across transactions is not allowed. s.clearJournalAndRefund() - return s.stateReadList + return s.stateAccessList } // IntermediateRoot computes the current root hash of the state trie. @@ -1052,9 +1097,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // SetTxContext sets the current transaction hash and index which are // used when the EVM emits new state logs. It should be invoked before // transaction execution. -func (s *StateDB) SetTxContext(thash common.Hash, ti int) { +func (s *StateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) { s.thash = thash s.txIndex = ti + s.blockAccessIndex = blockAccessIndex } func (s *StateDB) clearJournalAndRefund() { @@ -1355,7 +1401,7 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag // The reader update must be performed as the final step, otherwise, // the new state would not be visible before db.commit. - s.reader, _ = s.db.Reader(s.originalRoot) + s.reader, err = s.db.Reader(s.originalRoot) return ret, err } @@ -1435,7 +1481,7 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d s.transientStorage = newTransientStorage() if rules.IsAmsterdam { - s.stateReadList = bal.NewStateAccessList() + s.stateAccessList = bal.NewConstructionBlockAccessList() } } diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index c5faa7c98e..98d01343a4 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -234,7 +234,7 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { return s.inner.LogsForBurnAccounts() } -func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { +func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil { // Short circuit if no relevant hooks are set. return s.inner.Finalise(deleteEmptyObjects) @@ -244,7 +244,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { // that state change hooks will be invoked in deterministic // order when the accounts are deleted below var selfDestructedAddrs []common.Address - for addr := range s.inner.journal.dirties { + for addr := range s.inner.journal.mutations { obj := s.inner.stateObjects[addr] if obj == nil || !obj.selfDestructed { // Not self-destructed, keep searching. @@ -288,3 +288,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { } return s.inner.Finalise(deleteEmptyObjects) } + +func (s *hookedStateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) { + s.inner.SetTxContext(thash, ti, blockAccessIndex) +} diff --git a/core/state/statedb_hooked_test.go b/core/state/statedb_hooked_test.go index 6fe17ec1b4..fad234f848 100644 --- a/core/state/statedb_hooked_test.go +++ b/core/state/statedb_hooked_test.go @@ -82,7 +82,7 @@ func TestBurn(t *testing.T) { // TestHooks is a basic sanity-check of all hooks func TestHooks(t *testing.T) { inner, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) - inner.SetTxContext(common.Hash{0x11}, 100) // For the log + inner.SetTxContext(common.Hash{0x11}, 100, 101) // For the log var result []string var wants = []string{ "0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)", diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index b5ef42b3e0..0bf9b50e7b 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -662,26 +662,30 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error { return fmt.Errorf("got GetLogs(common.Hash{}) == %v, want GetLogs(common.Hash{}) == %v", state.GetLogs(common.Hash{}, 0, common.Hash{}, 0), checkstate.GetLogs(common.Hash{}, 0, common.Hash{}, 0)) } - if !maps.Equal(state.journal.dirties, checkstate.journal.dirties) { - getKeys := func(dirty map[common.Address]int) string { - var keys []common.Address - out := new(strings.Builder) - for key := range dirty { - keys = append(keys, key) - } - slices.SortFunc(keys, common.Address.Cmp) - for i, key := range keys { - fmt.Fprintf(out, " %d. %v\n", i, key) - } - return out.String() - } - have := getKeys(state.journal.dirties) - want := getKeys(checkstate.journal.dirties) - return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", have, want) + if !equalMutationSets(state.journal.mutations, checkstate.journal.mutations) { + return fmt.Errorf("journal mutation set mismatch.\nhave:\n%v\nwant:\n%v\n", state.journal.mutations, checkstate.journal.mutations) } return nil } +// equalMutationSets checks that two journal mutation maps have the same set of +// addresses and, for each address, the same per-kind counts. The stashed +// original values are ignored because comparing them across two independent +// state databases (with distinct pointer identities) isn't the point of this +// check — we only care that the two journals agree on what was touched. +func equalMutationSets(a, b map[common.Address]*journalMutationState) bool { + if len(a) != len(b) { + return false + } + for addr, sa := range a { + sb, ok := b[addr] + if !ok || sa.counts != sb.counts { + return false + } + } + return true +} + func TestTouchDelete(t *testing.T) { s := newStateEnv() s.state.getOrNewStateObject(common.Address{}) @@ -691,12 +695,54 @@ func TestTouchDelete(t *testing.T) { snapshot := s.state.Snapshot() s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified) - if len(s.state.journal.dirties) != 1 { - t.Fatal("expected one dirty state object") + if len(s.state.journal.mutations) != 1 { + t.Fatal("expected one mutated state object") } s.state.RevertToSnapshot(snapshot) - if len(s.state.journal.dirties) != 0 { - t.Fatal("expected no dirty state object") + if len(s.state.journal.mutations) != 0 { + t.Fatal("expected no journal mutations") + } +} + +func TestJournalMutationTracking(t *testing.T) { + state, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x02") + + if _, ok := state.journal.mutations[addr]; ok { + t.Fatal("unexpected initial mutation entry") + } + snapshot := state.Snapshot() + + state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + state.SetNonce(addr, 2, tracing.NonceChangeUnspecified) + state.SetCode(addr, []byte{0x1}, tracing.CodeChangeUnspecified) + state.SetState(addr, key, common.Hash{0x3}) + + want := journalMutationCounts{ + journalMutationKindCreate: 1, + journalMutationKindBalance: 1, + journalMutationKindNonce: 1, + journalMutationKindCode: 1, + journalMutationKindStorage: 1, + } + checkCounts := func(got *journalMutationState, label string) { + t.Helper() + if got == nil { + t.Fatalf("%s: missing mutation entry for %x", label, addr) + } + if got.counts != want { + t.Fatalf("%s: counts=%+v, want=%+v", label, got.counts, want) + } + } + checkCounts(state.journal.mutations[addr], "state") + + copy := state.Copy() + checkCounts(copy.journal.mutations[addr], "copy") + + state.RevertToSnapshot(snapshot) + if _, ok := state.journal.mutations[addr]; ok { + t.Fatalf("unexpected mutation entry after revert") } } diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index ed292d0beb..d99611ff2c 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -104,7 +104,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c // Disable the nonce check msg.SkipNonceChecks = true - stateCpy.SetTxContext(tx.Hash(), i) + stateCpy.SetTxContext(tx.Hash(), i, uint32(i+1)) // We attempt to apply a transaction. The goal is not to execute // the transaction successfully, rather to warm up touched data slots. diff --git a/core/state_processor.go b/core/state_processor.go index 54ebbd047b..5690a152e7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -27,11 +27,13 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" ) // StateProcessor is a basic Processor, which takes care of transitioning @@ -75,27 +77,20 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if hooks := cfg.Tracer; hooks != nil { tracingStateDB = state.NewHookedState(statedb, hooks) } - // Mutate the block and state according to any hard-fork specs if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(tracingStateDB) } var ( - context vm.BlockContext - signer = types.MakeSigner(config, header.Number, header.Time) + context = NewEVMBlockContext(header, p.chain, nil) + signer = types.MakeSigner(config, header.Number, header.Time) + evm = vm.NewEVM(context, tracingStateDB, config, cfg) + blockAccessList = bal.NewConstructionBlockAccessList() ) - - // Apply pre-execution system calls. - context = NewEVMBlockContext(header, p.chain, nil) - evm := vm.NewEVM(context, tracingStateDB, config, cfg) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if config.IsPrague(block.Number(), block.Time()) || config.IsUBT(block.Number(), block.Time()) { - ProcessParentBlockHash(block.ParentHash(), evm) - } + // Run the pre-execution system calls + blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { @@ -103,66 +98,97 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if err != nil { return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) _, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM", telemetry.StringAttribute("tx.hash", tx.Hash().Hex()), telemetry.Int64Attribute("tx.index", int64(i)), ) - - receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) + receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { spanEnd(&err) return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) + blockAccessList.Merge(bal) spanEnd(nil) } - requests, err := postExecution(ctx, config, block, allLogs, evm) + requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) if err != nil { return nil, err } + blockAccessList.Merge(bal) - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + // Finalize the block, applying any consensus engine specific extras + // (e.g. block rewards). + // + // TODO(rjl493456442) integrate it into the PostExecution. + p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList) return &ProcessResult{ Receipts: receipts, Requests: requests, Logs: allLogs, GasUsed: gp.Used(), + Bal: blockAccessList, }, nil } -// postExecution processes the post-execution system calls if Prague is enabled. -func postExecution(ctx context.Context, config *params.ChainConfig, block *types.Block, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) { +// PreExecution processes pre-execution system calls. +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { + _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") + defer spanEnd(nil) + + var blockAccessList *bal.ConstructionBlockAccessList + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } + // EIP-4788 + if beaconRoot != nil { + ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) + } + // EIP-2935 + if config.IsPrague(number, time) || config.IsUBT(number, time) { + ProcessParentBlockHash(parent, evm, blockAccessList) + } + return blockAccessList +} + +// PostExecution processes post-execution system calls when Prague is enabled. +// If Prague is not activated, it returns null requests to differentiate from +// empty requests. +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } // Read requests if Prague is enabled. - if config.IsPrague(block.Number(), block.Time()) { + if config.IsPrague(number, time) { + rules := config.Rules(number, true, time) // IsMerge is always true + requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { - return requests, fmt.Errorf("failed to parse deposit logs: %w", err) + return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process withdrawal queue: %w", err) + if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process consolidation queue: %w", err) + if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } - - return requests, nil + return requests, blockAccessList, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, bal *bal.ConstructionBlockAccessList, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -174,12 +200,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { - return nil, err + return nil, nil, err } // Update the state with pending changes. var root []byte if evm.ChainConfig().IsByzantium(blockNumber) { - evm.StateDB.Finalise(true) + bal = evm.StateDB.Finalise(true) } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } @@ -188,7 +214,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, if statedb.Database().Type().Is(state.TypeUBT) { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil } // MakeReceipt generates the receipt object for a transaction given its execution result. @@ -233,10 +259,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // and uses the input parameters for its environment. It returns the receipt // for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { - return nil, err + return nil, nil, err } // Create a new context to be used in the EVM environment return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) @@ -244,7 +270,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. -func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { +func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -254,24 +280,26 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: ¶ms.BeaconRootsAddress, Data: beaconRoot[:], } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) + evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessParentBlockHash stores the parent block hash in the history storage contract // as per EIP-2935/7709. -func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { +func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -281,13 +309,15 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: ¶ms.HistoryStorageAddress, Data: prevHash.Bytes(), } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) + evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if err != nil { @@ -296,22 +326,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) error { - return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress) +func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) error { - return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress) +func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error { +func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -321,21 +351,25 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) + evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + bal := evm.StateDB.Finalise(true) if err != nil { return fmt.Errorf("system call failed to execute: %v", err) } + blockAccessList.Merge(bal) + if len(ret) == 0 { return nil // skip empty output } @@ -378,8 +412,16 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) { // AssembleBlock finalizes the state and assembles the block with provided // body and receipts. -func AssembleBlock(engine consensus.Engine, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) *types.Block { - engine.Finalize(chain, header, state, body) +func AssembleBlock(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, blockAccessList *bal.ConstructionBlockAccessList) *types.Block { + // Assign the post-transition state root header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) - return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + + if !chain.Config().IsAmsterdam(header.Number, header.Time) { + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + } + // Assign the BlockAccessListHash if Amsterdam has been enabled + bal := blockAccessList.ToEncodingObj() + balHash := bal.Hash() + header.BlockAccessListHash = &balHash + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)).WithAccessListUnsafe(bal) } diff --git a/core/state_transition.go b/core/state_transition.go index c7b0593857..51c5836892 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,7 +68,7 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860 bool) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) { // Set the starting gas for the raw transaction var gas uint64 if isContractCreation && isHomestead { @@ -107,8 +107,32 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set } } if accessList != nil { - gas += uint64(len(accessList)) * params.TxAccessListAddressGas - gas += uint64(accessList.StorageKeys()) * params.TxAccessListStorageKeyGas + addresses := uint64(len(accessList)) + storageKeys := uint64(accessList.StorageKeys()) + if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += storageKeys * params.TxAccessListStorageKeyGas + + // EIP-7981: access list data is charged in addition to the base charge. + if isAmsterdam { + const ( + addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + ) + if (math.MaxUint64-gas)/addressCost < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += addresses * addressCost + if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += storageKeys * storageKeyCost + } } if authList != nil { gas += uint64(len(authList)) * params.CallNewAccountGas @@ -117,7 +141,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set } // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). -func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { +func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -125,15 +149,41 @@ func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { if rules.IsAmsterdam { // EIP-7976 changes how calldata is priced. // From 10/40 to 64/64 for zero/non-zero bytes. - tokens = uint64(len(data)) * params.TxTokenPerNonZeroByte tokenCost = params.TxCostFloorPerToken7976 + dataLen := uint64(len(data)) + if math.MaxUint64/params.TxTokenPerNonZeroByte < dataLen { + return 0, ErrGasUintOverflow + } + tokens = dataLen * params.TxTokenPerNonZeroByte + + // EIP-7981 adds additional tokens for every entry in the accesslist + const addressTokenCost = uint64(common.AddressLength) * params.TxTokenPerNonZeroByte + addresses := uint64(len(accessList)) + if (math.MaxUint64-tokens)/addressTokenCost < addresses { + return 0, ErrGasUintOverflow + } + tokens += addresses * addressTokenCost + + const storageKeyTokenCost = uint64(common.HashLength) * params.TxTokenPerNonZeroByte + storageKeys := uint64(accessList.StorageKeys()) + if (math.MaxUint64-tokens)/storageKeyTokenCost < storageKeys { + return 0, ErrGasUintOverflow + } + tokens += storageKeys * storageKeyTokenCost } else { var ( z = uint64(bytes.Count(data, []byte{0})) nz = uint64(len(data)) - z ) // Pre-Amsterdam - tokens = nz*params.TxTokenPerNonZeroByte + z + if math.MaxUint64/params.TxTokenPerNonZeroByte < nz { + return 0, ErrGasUintOverflow + } + tokens = nz * params.TxTokenPerNonZeroByte + if math.MaxUint64-tokens < z { + return 0, ErrGasUintOverflow + } + tokens += z tokenCost = params.TxCostFloorPerToken } @@ -160,14 +210,14 @@ type Message struct { To *common.Address From common.Address Nonce uint64 - Value *big.Int + Value *uint256.Int GasLimit uint64 - GasPrice *big.Int - GasFeeCap *big.Int - GasTipCap *big.Int + GasPrice *uint256.Int + GasFeeCap *uint256.Int + GasTipCap *uint256.Int Data []byte AccessList types.AccessList - BlobGasFeeCap *big.Int + BlobGasFeeCap *uint256.Int BlobHashes []common.Hash SetCodeAuthorizations []types.SetCodeAuthorization @@ -188,32 +238,64 @@ type Message struct { // TransactionToMessage converts a transaction into a Message. func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) { + from, err := types.Sender(s, tx) + if err != nil { + return nil, err + } + gasPrice, overflow := uint256.FromBig(tx.GasPrice()) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + from.Hex(), tx.GasPrice().BitLen()) + } + txGasFeeCap := tx.GasFeeCap() + gasFeeCap, overflow := uint256.FromBig(txGasFeeCap) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + from.Hex(), tx.GasFeeCap().BitLen()) + } + txGasTipCap := tx.GasTipCap() + gasTipCap, overflow := uint256.FromBig(txGasTipCap) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, + from.Hex(), tx.GasTipCap().BitLen()) + } + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return nil, fmt.Errorf("value exceeds 256 bits: address %v", from.Hex()) + } + blobGasFeeCap, overflow := uint256.FromBig(tx.BlobGasFeeCap()) + if overflow { + return nil, fmt.Errorf("blobGasFeeCap exceeds 256 bits: address %v", from.Hex()) + } + msg := &Message{ + From: from, Nonce: tx.Nonce(), GasLimit: tx.Gas(), - GasPrice: tx.GasPrice(), - GasFeeCap: tx.GasFeeCap(), - GasTipCap: tx.GasTipCap(), + GasPrice: gasPrice, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, To: tx.To(), - Value: tx.Value(), + Value: value, Data: tx.Data(), AccessList: tx.AccessList(), SetCodeAuthorizations: tx.SetCodeAuthorizations(), SkipNonceChecks: false, SkipTransactionChecks: false, BlobHashes: tx.BlobHashes(), - BlobGasFeeCap: tx.BlobGasFeeCap(), + BlobGasFeeCap: blobGasFeeCap, } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { - msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) - if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { - msg.GasPrice = msg.GasFeeCap + effectiveGasPrice := new(big.Int).Add(baseFee, txGasTipCap) + if effectiveGasPrice.Cmp(txGasFeeCap) > 0 { + effectiveGasPrice = txGasFeeCap } + // EffectiveGasPrice is already capped by txGasFeeCap, therefore + // the overflow check is not required. + msg.GasPrice = uint256.MustFromBig(effectiveGasPrice) } - var err error - msg.From, err = types.Sender(s, tx) - return msg, err + return msg, nil } // ApplyMessage computes the new state by applying the given message @@ -283,46 +365,70 @@ func (st *stateTransition) to() common.Address { } func (st *stateTransition) buyGas() error { - mgval := new(big.Int).SetUint64(st.msg.GasLimit) - mgval.Mul(mgval, st.msg.GasPrice) - balanceCheck := new(big.Int).Set(mgval) + mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) + _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + balanceCheck := new(uint256.Int).Set(mgval) if st.msg.GasFeeCap != nil { balanceCheck.SetUint64(st.msg.GasLimit) - balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap) + if _, overflow := balanceCheck.MulOverflow(balanceCheck, st.msg.GasFeeCap); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + } + if st.msg.Value != nil { + if _, overflow := balanceCheck.AddOverflow(balanceCheck, st.msg.Value); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } } - balanceCheck.Add(balanceCheck, st.msg.Value) if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { if blobGas := st.blobGasUsed(); blobGas > 0 { // Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap - blobBalanceCheck := new(big.Int).SetUint64(blobGas) - blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap) - balanceCheck.Add(balanceCheck, blobBalanceCheck) + blobBalanceCheck := new(uint256.Int).SetUint64(blobGas) + if _, overflow := blobBalanceCheck.MulOverflow(blobBalanceCheck, st.msg.BlobGasFeeCap); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + if _, overflow := balanceCheck.AddOverflow(balanceCheck, blobBalanceCheck); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } // Pay for blobGasUsed * actual blob fee - blobFee := new(big.Int).SetUint64(blobGas) - blobFee.Mul(blobFee, st.evm.Context.BlobBaseFee) - mgval.Add(mgval, blobFee) + blobBaseFee, overflow := uint256.FromBig(st.evm.Context.BlobBaseFee) + if overflow { + return fmt.Errorf("invalid blobBaseFee: %v", st.evm.Context.BlobBaseFee) + } + blobFee := new(uint256.Int).SetUint64(blobGas) + + // In practice, overflow checking is unnecessary, as blobBaseFee cannot exceed + // BlobGasFeeCap. However, in eth_call it is still possible for users to specify + // an excessively large blob base fee and bypass the blob base fee validation. + _, overflow = blobFee.MulOverflow(blobFee, blobBaseFee) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + _, overflow = mgval.AddOverflow(mgval, blobFee) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } } } - balanceCheckU256, overflow := uint256.FromBig(balanceCheck) - if overflow { - return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) - } - if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { + if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } if err := st.gp.SubGas(st.msg.GasLimit); err != nil { return err } - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil { - st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) + if st.evm.Config.Tracer.HasGasHook() { + empty := vm.GasBudget{} + initial := vm.NewGasBudget(st.msg.GasLimit) + st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance) } st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) st.initialBudget = st.gasRemaining.Copy() - mgvalU256, _ := uint256.FromBig(mgval) - st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } @@ -362,21 +468,13 @@ func (st *stateTransition) preCheck() error { // Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { - if l := msg.GasFeeCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, - msg.From.Hex(), l) - } - if l := msg.GasTipCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, - msg.From.Hex(), l) - } if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap) } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. - if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 { + if msg.GasFeeCap.CmpBig(st.evm.Context.BaseFee) < 0 { return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee) } @@ -410,7 +508,7 @@ func (st *stateTransition) preCheck() error { if !skipCheck { // This will panic if blobBaseFee is nil, but blobBaseFee presence // is verified as part of header validation. - if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 { + if msg.BlobGasFeeCap.CmpBig(st.evm.Context.BlobBaseFee) < 0 { return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee) } @@ -462,7 +560,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { floorDataGas uint64 ) // Check clauses 4-5, subtract intrinsic gas if everything is correct - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return nil, err } @@ -470,12 +568,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if !sufficient { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) } - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } // Gas limit suffices for the floor data cost (EIP-7623) if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data) + floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) if err != nil { return nil, err } @@ -493,9 +591,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Check clause 6 - value, overflow := uint256.FromBig(msg.Value) - if overflow { - return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) + value := msg.Value + if value == nil { + value = new(uint256.Int) } if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, value) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) @@ -510,7 +608,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the preparatory steps for state transition which includes: // - prepare accessList(post-berlin) - // - reset transient storage(eip 1153) + // - reset transient storage(EIP-1153) + // - enable block-level accessList construction (EIP-7928) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) var ( @@ -555,8 +654,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // After EIP-7623: Data-heavy transactions pay the floor gas. if used := st.gasUsed(); used < floorDataGas { prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used}) - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) } } if peakGasUsed < floorDataGas { @@ -579,9 +678,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } effectiveTip := msg.GasPrice if rules.IsLondon { - effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee) + baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee) + if overflow { + return nil, fmt.Errorf("invalid baseFee: %v", st.evm.Context.BaseFee) + } + effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee) } - effectiveTipU256, _ := uint256.FromBig(effectiveTip) if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields @@ -589,7 +691,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // the coinbase when simulating calls. } else { fee := new(uint256.Int).SetUint64(st.gasUsed()) - fee.Mul(fee, effectiveTipU256) + fee.Mul(fee, effectiveTip) st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) // add the coinbase to the witness iff the fee is greater than 0 @@ -681,8 +783,11 @@ func (st *stateTransition) calcRefund() vm.GasBudget { if refund > st.state.GetRefund() { refund = st.state.GetRefund() } - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 { - st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, st.gasRemaining.RegularGas+refund, tracing.GasChangeTxRefunds) + if refund > 0 && st.evm.Config.Tracer.HasGasHook() { + after := st.gasRemaining + after.RegularGas += refund + + st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds) } return vm.NewGasBudget(refund) } @@ -691,11 +796,13 @@ func (st *stateTransition) calcRefund() vm.GasBudget { // exchanged at the original rate. func (st *stateTransition) returnGas() { remaining := uint256.NewInt(st.gasRemaining.RegularGas) - remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) + remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 { - st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, 0, tracing.GasChangeTxLeftOverReturned) + if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() { + after := st.gasRemaining + after.RegularGas = 0 + st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned) } } diff --git a/core/state_transition_test.go b/core/state_transition_test.go new file mode 100644 index 0000000000..8aab016123 --- /dev/null +++ b/core/state_transition_test.go @@ -0,0 +1,287 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +func TestFloorDataGas(t *testing.T) { + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + key1 := common.HexToHash("0xaa") + key2 := common.HexToHash("0xbb") + + tests := []struct { + name string + amsterdam bool + data []byte + accessList types.AccessList + want uint64 + }{ + { + name: "pre-amsterdam/empty", + want: params.TxGas, + }, + { + name: "pre-amsterdam/zero-bytes-only", + data: bytes.Repeat([]byte{0x00}, 100), + // 100 zero tokens * 10 cost = 1000 + want: params.TxGas + 100*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/non-zero-bytes-only", + data: bytes.Repeat([]byte{0xff}, 100), + // 100 nz * 4 tokens * 10 cost = 4000 + want: params.TxGas + 100*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/mixed", + data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), + // 50 zero + 50*4 nz = 250 tokens * 10 = 2500 + want: params.TxGas + (50+50*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/access-list-ignored", + data: bytes.Repeat([]byte{0xff}, 10), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + }, + // pre-amsterdam: floor calculation does not include access list + want: params.TxGas + 10*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken, + }, + { + name: "amsterdam/empty", + amsterdam: true, + want: params.TxGas, + }, + { + name: "amsterdam/data-only", + amsterdam: true, + data: bytes.Repeat([]byte{0x00}, 1024), + // post-amsterdam: every byte = 4 tokens regardless of value + want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/data-non-zero", + amsterdam: true, + data: bytes.Repeat([]byte{0xff}, 1024), + // same as zero data post-amsterdam + want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/access-list-addresses-only", + amsterdam: true, + accessList: types.AccessList{ + {Address: addr1}, + {Address: addr2}, + }, + // 2 * 20 bytes * 4 tokens/byte * 16 cost/token + want: params.TxGas + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/access-list-with-storage-keys", + amsterdam: true, + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + }, + // 1 addr * 20 * 4 + 2 keys * 32 * 4 = 80 + 256 = 336 tokens * 16 + want: params.TxGas + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/mixed", + amsterdam: true, + data: bytes.Repeat([]byte{0xff}, 100), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + {Address: addr2, StorageKeys: []common.Hash{key1, key2}}, + }, + // data: 100*4 = 400; addrs: 2*20*4 = 160; keys: 3*32*4 = 384; total = 944 * 16 + want: params.TxGas + (100*params.TxTokenPerNonZeroByte+2*common.AddressLength*params.TxTokenPerNonZeroByte+3*common.HashLength*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken7976, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := params.Rules{IsAmsterdam: tt.amsterdam} + got, err := FloorDataGas(rules, tt.data, tt.accessList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("gas mismatch: got %d, want %d", got, tt.want) + } + }) + } +} + +func TestIntrinsicGas(t *testing.T) { + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + key1 := common.HexToHash("0xaa") + key2 := common.HexToHash("0xbb") + + const ( + amsterdamAddressCost = uint64(common.AddressLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 1280 + amsterdamStorageKeyCost = uint64(common.HashLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 2048 + ) + + tests := []struct { + name string + data []byte + accessList types.AccessList + authList []types.SetCodeAuthorization + creation bool + isHomestead bool + isEIP2028 bool + isEIP3860 bool + isAmsterdam bool + want uint64 + }{ + { + name: "frontier/empty-call", + want: params.TxGas, + }, + { + name: "frontier/contract-creation-pre-homestead", + creation: true, + isHomestead: false, + // pre-homestead, contract creation still uses TxGas + want: params.TxGas, + }, + { + name: "homestead/contract-creation", + creation: true, + isHomestead: true, + want: params.TxGasContractCreation, + }, + { + name: "frontier/non-zero-data", + data: bytes.Repeat([]byte{0xff}, 100), + // 100 nz bytes * 68 (frontier) + want: params.TxGas + 100*params.TxDataNonZeroGasFrontier, + }, + { + name: "istanbul/non-zero-data", + data: bytes.Repeat([]byte{0xff}, 100), + isEIP2028: true, + // 100 nz bytes * 16 (post-EIP2028) + want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028, + }, + { + name: "istanbul/zero-data", + data: bytes.Repeat([]byte{0x00}, 100), + isEIP2028: true, + // 100 zero bytes * 4 + want: params.TxGas + 100*params.TxDataZeroGas, + }, + { + name: "istanbul/mixed-data", + data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), + isEIP2028: true, + want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028, + }, + { + name: "shanghai/init-code-word-gas", + data: bytes.Repeat([]byte{0x00}, 64), // 2 words + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 + want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + }, + { + name: "shanghai/init-code-non-multiple-of-32", + data: bytes.Repeat([]byte{0x00}, 33), // 2 words (rounded up) + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas, + }, + { + name: "berlin/access-list", + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + {Address: addr2, StorageKeys: []common.Hash{key1}}, + }, + isEIP2028: true, + // 2 addrs * 2400 + 3 keys * 1900 + want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas, + }, + { + name: "amsterdam/access-list-extra-cost", + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + {Address: addr2, StorageKeys: []common.Hash{key1}}, + }, + isEIP2028: true, + isAmsterdam: true, + // base access-list charge + EIP-7981 extra + want: params.TxGas + + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, + }, + { + name: "prague/auth-list", + authList: []types.SetCodeAuthorization{ + {Address: addr1}, + {Address: addr2}, + {Address: addr1}, + }, + isEIP2028: true, + // 3 auths * 25000 + want: params.TxGas + 3*params.CallNewAccountGas, + }, + { + name: "amsterdam/combined", + data: bytes.Repeat([]byte{0xff}, 100), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + }, + authList: []types.SetCodeAuthorization{ + {Address: addr2}, + }, + isEIP2028: true, + isAmsterdam: true, + want: params.TxGas + + 100*params.TxDataNonZeroGasEIP2028 + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + + 1*params.CallNewAccountGas, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, + tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := vm.GasCosts{RegularGas: tt.want} + if got != want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, want) + } + }) + } +} diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 62c62ac3b3..6ea3f7ebbf 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -164,10 +164,36 @@ type ( // FaultHook is invoked when an error occurs during the execution of an opcode. FaultHook = func(pc uint64, op byte, gas, cost uint64, scope OpContext, depth int, err error) - // GasChangeHook is invoked when the gas changes. + // GasChangeHook reports changes to the regular execution gas. Tracers + // that don't need visibility into the state-access gas dimension + // introduced by EIP-8037 (Amsterdam) can implement only this hook; it + // will continue to fire across the Amsterdam fork unchanged. + // + // If both this hook and GasChangeHookV2 are implemented on the same + // tracer, only V2 will be invoked. Implement exactly one to avoid + // double-counting. GasChangeHook = func(old, new uint64, reason GasChangeReason) - // TODO(sina, rjl), please add GasChangeV2Hook by landing the multi-dimensional gas + // GasChangeHookV2 is invoked when any gas dimension changes. It is the + // multi-dimensional successor to GasChangeHook, exposing the state-access + // gas dimension introduced by EIP-8037 (Amsterdam) alongside the regular + // dimension. + // + // Compatibility: + // - Post-Amsterdam: fires for changes to either the regular or the + // state-access dimension. The non-changing dimension is passed through + // unchanged in both `old` and `new` so consumers always observe the + // complete gas vector. + // - Pre-Amsterdam: no state-access gas events occur, so the State field + // of both `old` and `new` is always zero. Tracers that register only + // V2 still receive every regular-gas change as Gas{State: 0} and + // behave identically to a V1 tracer; there is no pre-Amsterdam event + // a V2-only tracer misses. + // + // V1 and V2 coexist: when both are registered on a tracer, only V2 is + // invoked. Tracers SHOULD register at most one of the two to avoid + // double-counting. + GasChangeHookV2 = func(old, new Gas, reason GasChangeReason) /* - Chain events - @@ -250,13 +276,14 @@ type ( type Hooks struct { // VM events - OnTxStart TxStartHook - OnTxEnd TxEndHook - OnEnter EnterHook - OnExit ExitHook - OnOpcode OpcodeHook - OnFault FaultHook - OnGasChange GasChangeHook + OnTxStart TxStartHook + OnTxEnd TxEndHook + OnEnter EnterHook + OnExit ExitHook + OnOpcode OpcodeHook + OnFault FaultHook + OnGasChange GasChangeHook + OnGasChangeV2 GasChangeHookV2 // Chain events OnBlockchainInit BlockchainInitHook OnClose CloseHook @@ -280,6 +307,35 @@ type Hooks struct { OnBlockHashRead BlockHashReadHook } +// HasGasHook reports whether any gas-change hook is registered. Call sites +// should use this to short-circuit before constructing the Gas / GasBudget +// arguments to EmitGasChange when tracing is off — the dispatch is otherwise +// always paid the cost of evaluating those args. +func (h *Hooks) HasGasHook() bool { + return h != nil && (h.OnGasChangeV2 != nil || h.OnGasChange != nil) +} + +// EmitGasChange dispatches a gas change event to the registered hooks. If the +// multi-dimensional OnGasChangeV2 hook is set it is invoked with the full Gas +// vectors; otherwise the single-dimensional OnGasChange hook is invoked with +// the regular-gas dimension only. The call is a no-op when the receiver is +// nil, when neither hook is registered, or when the reason is GasChangeIgnored. +// +// Call sites SHOULD use this helper instead of invoking the hooks directly so +// that both variants stay consistent across the Amsterdam fork boundary. +func (h *Hooks) EmitGasChange(old, new Gas, reason GasChangeReason) { + if h == nil || reason == GasChangeIgnored { + return + } + if h.OnGasChangeV2 != nil { + h.OnGasChangeV2(old, new, reason) + return + } + if h.OnGasChange != nil { + h.OnGasChange(old.Regular, new.Regular, reason) + } +} + // BalanceChangeReason is used to indicate the reason for a balance change, useful // for tracing and reporting. type BalanceChangeReason byte @@ -335,6 +391,19 @@ const ( BalanceChangeRevert BalanceChangeReason = 15 ) +// Gas represents a multi-dimensional gas budget introduced by EIP-8037. +// It carries the regular execution gas and the state-access gas, which are +// metered independently from the Amsterdam fork onwards. +// +// Before Amsterdam, gas metering is single-dimensional and only the Regular +// field is meaningful; State is always zero. The struct is shaped so that +// pre-Amsterdam call sites can populate it as Gas{Regular: g} without loss +// of fidelity relative to the legacy single-uint64 hook. +type Gas struct { + Regular uint64 // Regular is the budget for ordinary execution gas. + State uint64 // State is the budget dedicated to state-access gas (zero pre-Amsterdam). +} + // GasChangeReason is used to indicate the reason for a gas change, useful // for tracing and reporting. // diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 4030a0c339..d33629365f 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -116,6 +116,8 @@ const ( announceThreshold = -1 ) +var errLegacyTx = errors.New("legacy transaction format") + // blobTxMeta is the minimal subset of types.BlobTx necessary to validate and // schedule the blob transactions into the following blocks. Only ever add the // bare minimum needed fields to keep the size down (and thus number of entries @@ -147,28 +149,137 @@ type blobTxMeta struct { evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces } -// newBlobTxMeta retrieves the indexed metadata fields from a blob transaction -// and assembles a helper struct to track in memory. -// Requires the transaction to have a sidecar (or that we introduce a special version tag for no-sidecar). -func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transaction) *blobTxMeta { - if tx.BlobTxSidecar() == nil { - // This should never happen, as the pool only admits blob transactions with a sidecar +// blobTxForPool is the storage representation of a blob transaction in the +// blobpool. +type blobTxForPool struct { + Tx *types.Transaction // tx without sidecar + Version byte + Commitments []kzg4844.Commitment + Proofs []kzg4844.Proof + Blobs []kzg4844.Blob +} + +// Sidecar returns BlobTxSidecar of ptx. +func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar { + return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) +} + +// ApplySidecar copies the sidecar's fields into the flat fields. +func (ptx *blobTxForPool) ApplySidecar(sc *types.BlobTxSidecar) { + ptx.Version = sc.Version + ptx.Commitments = sc.Commitments + ptx.Proofs = sc.Proofs + ptx.Blobs = sc.Blobs +} + +// TxSize returns the transaction size on the network without +// reconstructing the transaction. +func (ptx *blobTxForPool) TxSize() uint64 { + var blobs, commitments, proofs uint64 + for i := range ptx.Blobs { + blobs += rlp.BytesSize(ptx.Blobs[i][:]) + } + for i := range ptx.Commitments { + commitments += rlp.BytesSize(ptx.Commitments[i][:]) + } + for i := range ptx.Proofs { + proofs += rlp.BytesSize(ptx.Proofs[i][:]) + } + return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs)) +} + +// ToTx reconstructs a full Transaction with the sidecar attached. +func (ptx *blobTxForPool) ToTx() *types.Transaction { + return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar()) +} + +// newBlobTxForPool decomposes a blob transaction into blobTxForPool type. +func newBlobTxForPool(tx *types.Transaction) *blobTxForPool { + sc := tx.BlobTxSidecar() + if sc == nil { panic("missing blob tx sidecar") } + return &blobTxForPool{ + Tx: tx.WithoutBlobTxSidecar(), + Version: sc.Version, + Commitments: sc.Commitments, + Proofs: sc.Proofs, + Blobs: sc.Blobs, + } +} + +// encodeForNetwork transforms stored blobTxForPool RLP into the standard +// network transaction encoding. This is used for getRLP. +// +// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]] +// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]]) +// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]]) +func encodeForNetwork(storedRLP []byte) ([]byte, error) { + elems, err := rlp.SplitListValues(storedRLP) + if err != nil { + return nil, fmt.Errorf("invalid blobTxForPool RLP: %w", err) + } + if len(elems) < 5 { + return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems)) + } + + // 1. Extract tx byte and other tx fields + txBytes, _, err := rlp.SplitString(elems[0]) + if err != nil { + return nil, fmt.Errorf("invalid tx bytes: %w", err) + } + if len(txBytes) < 2 { + return nil, errors.New("tx bytes too short") + } + typeByte := txBytes[0] + txRLP := txBytes[1:] + + // 2. Find the version of sidecar. + version, _, err := rlp.SplitUint64(elems[1]) + if err != nil || version > 255 { + return nil, fmt.Errorf("invalid version: %w", err) + } + versionByte := byte(version) + // 3. Extract sidecar elements. + commitmentsRLP := elems[2] + proofsRLP := elems[3] + blobsRLP := elems[4] + + // 4. Reconstruct into the network format. + var outer [][]byte + if versionByte == types.BlobSidecarVersion0 { + outer = [][]byte{txRLP, blobsRLP, commitmentsRLP, proofsRLP} + } else { + outer = [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP} + } + body, err := rlp.MergeListValues(outer) + if err != nil { + return nil, err + } + // Prepend type byte and wrap as an RLP string. + inner := make([]byte, 1+len(body)) + inner[0] = typeByte + copy(inner[1:], body) + return rlp.EncodeToBytes(inner) +} + +// newBlobTxMeta retrieves the indexed metadata fields from a pooled blob +// transaction and assembles a helper struct to track in memory. +func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta { meta := &blobTxMeta{ - hash: tx.Hash(), - vhashes: tx.BlobHashes(), - version: tx.BlobTxSidecar().Version, + hash: ptx.Tx.Hash(), + vhashes: ptx.Tx.BlobHashes(), + version: ptx.Version, id: id, storageSize: storageSize, size: size, - nonce: tx.Nonce(), - costCap: uint256.MustFromBig(tx.Cost()), - execTipCap: uint256.MustFromBig(tx.GasTipCap()), - execFeeCap: uint256.MustFromBig(tx.GasFeeCap()), - blobFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap()), - execGas: tx.Gas(), - blobGas: tx.BlobGas(), + nonce: ptx.Tx.Nonce(), + costCap: uint256.MustFromBig(ptx.Tx.Cost()), + execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()), + execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()), + blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()), + execGas: ptx.Tx.Gas(), + blobGas: ptx.Tx.BlobGas(), } meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap) meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap) @@ -460,10 +571,17 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser return err } // Index all transactions on disk and delete anything unprocessable - var fails []uint64 + var ( + toDelete []uint64 + convertTxs []uint64 + ) index := func(id uint64, size uint32, blob []byte) { - if p.parseTransaction(id, size, blob) != nil { - fails = append(fails, id) + err := p.parseTransaction(id, size, blob) + if err != nil { + toDelete = append(toDelete, id) + } + if errors.Is(err, errLegacyTx) { + convertTxs = append(convertTxs, id) } } store, err := billy.Open(billy.Options{Path: queuedir, Repair: true}, slotter, index) @@ -472,17 +590,58 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser } p.store = store - if len(fails) > 0 { - log.Warn("Dropping invalidated blob transactions", "ids", fails) - dropInvalidMeter.Mark(int64(len(fails))) + // Migrate legacy transactions (types.Transaction) to pooledBlobTx format. + if len(convertTxs) > 0 { + for _, id := range convertTxs { + var tx types.Transaction + data, err := p.store.Get(id) + if err != nil { + continue + } + err = rlp.DecodeBytes(data, &tx) + if err != nil { + continue + } + if tx.BlobTxSidecar() == nil { + continue + } + ptx := newBlobTxForPool(&tx) + blob, err := rlp.EncodeToBytes(ptx) + if err != nil { + continue + } + id, err := p.store.Put(blob) + if err != nil { + continue + } + meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx) - for _, id := range fails { + // If the newly inserted transaction fails to be tracked, + // it should also be removed with those in `toDelete` + sender, err := types.Sender(p.signer, ptx.Tx) + if err != nil { + toDelete = append(toDelete, id) + continue + } + if err := p.trackTransaction(meta, sender); err != nil { + toDelete = append(toDelete, id) + continue + } + } + } + + if len(toDelete) > 0 { + log.Warn("Dropping invalidated blob transactions", "ids", toDelete) + dropInvalidMeter.Mark(int64(len(toDelete))) + + for _, id := range toDelete { if err := p.store.Delete(id); err != nil { p.Close() return err } } } + // Sort the indexed transactions by nonce and delete anything gapped, create // the eviction heap of anyone still standing for addr := range p.index { @@ -558,36 +717,33 @@ func (p *BlobPool) Close() error { // parseTransaction is a callback method on pool creation that gets called for // each transaction on disk to create the in-memory metadata index. -// Announced state is not initialized here, it needs to be iniitalized seprately. +// Return value `bool` is set to true when the entry has old Transaction type. func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error { - tx := new(types.Transaction) - if err := rlp.DecodeBytes(blob, tx); err != nil { - // This path is impossible unless the disk data representation changes - // across restarts. For that ever improbable case, recover gracefully - // by ignoring this data entry. - log.Error("Failed to decode blob pool entry", "id", id, "err", err) + var ptx blobTxForPool + if err := rlp.DecodeBytes(blob, &ptx); err != nil { + kind, content, _, splitErr := rlp.Split(blob) + // check whether it is legacy tx type + if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 { + return errLegacyTx + } return err } - if tx.BlobTxSidecar() == nil { - log.Error("Missing sidecar in blob pool entry", "id", id, "hash", tx.Hash()) - return errors.New("missing blob sidecar") + meta := newBlobTxMeta(id, ptx.TxSize(), size, &ptx) + sender, err := types.Sender(p.signer, ptx.Tx) + if err != nil { + return err } + return p.trackTransaction(meta, sender) +} - meta := newBlobTxMeta(id, tx.Size(), size, tx) +// trackTransaction registers a transaction's metadata in the pool's indices. +func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error { if p.lookup.exists(meta.hash) { // This path is only possible after a crash, where deleted items are not // removed via the normal shutdown-startup procedure and thus may get // partially resurrected. - log.Error("Rejecting duplicate blob pool entry", "id", id, "hash", tx.Hash()) - return errors.New("duplicate blob entry") - } - sender, err := types.Sender(p.signer, tx) - if err != nil { - // This path is impossible unless the signature validity changes across - // restarts. For that ever improbable case, recover gracefully by ignoring - // this data entry. - log.Error("Failed to recover blob tx sender", "id", id, "hash", tx.Hash(), "err", err) - return err + log.Error("Rejecting duplicate blob pool entry", "id", meta.id, "hash", meta.hash) + return fmt.Errorf("duplicate blob entry %d, %s", meta.id, meta.hash) } if _, ok := p.index[sender]; !ok { if err := p.reserver.Hold(sender); err != nil { @@ -863,17 +1019,17 @@ func (p *BlobPool) offload(addr common.Address, nonce uint64, id uint64, inclusi log.Error("Blobs missing for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return } - var tx types.Transaction - if err = rlp.DecodeBytes(data, &tx); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return } - block, ok := inclusions[tx.Hash()] + block, ok := inclusions[ptx.Tx.Hash()] if !ok { log.Warn("Blob transaction swapped out by signer", "from", addr, "nonce", nonce, "id", id) return } - if err := p.limbo.push(&tx, block); err != nil { + if err := p.limbo.push(&ptx, block); err != nil { log.Warn("Failed to offload blob tx into limbo", "err", err) return } @@ -951,13 +1107,13 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - var tx types.Transaction - if err = rlp.DecodeBytes(data, &tx); err != nil { + var ptx blobTxForPool + if err = rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - announcable = append(announcable, tx.WithoutBlobTxSidecar()) - log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash()) + announcable = append(announcable, ptx.Tx) + log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", ptx.Tx.Hash()) } } } @@ -1108,7 +1264,7 @@ func (p *BlobPool) reorg(oldHead, newHead *types.Header) (map[common.Address][]* func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error { // Retrieve the associated blob from the limbo. Without the blobs, we cannot // add the transaction back into the pool as it is not mineable. - tx, err := p.limbo.pull(txhash) + ptx, err := p.limbo.pull(txhash) if err != nil { log.Error("Blobs unavailable, dropping reorged tx", "err", err) return err @@ -1124,30 +1280,29 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error { // could theoretically halt a Geth node for ~1.2s by reorging per block. However, // this attack is financially inefficient to execute. head := p.head.Load() - if p.chain.Config().IsOsaka(head.Number, head.Time) && tx.BlobTxSidecar().Version == types.BlobSidecarVersion0 { - if err := tx.BlobTxSidecar().ToV1(); err != nil { + if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.Version == types.BlobSidecarVersion0 { + sc := ptx.Sidecar() + if err := sc.ToV1(); err != nil { log.Error("Failed to convert the legacy sidecar", "err", err) return err } - log.Info("Legacy blob transaction is reorged", "hash", tx.Hash()) + ptx.ApplySidecar(sc) + log.Info("Legacy blob transaction is reorged", "hash", ptx.Tx.Hash()) } - // Serialize the transaction back into the primary datastore. - blob, err := rlp.EncodeToBytes(tx) + blob, err := rlp.EncodeToBytes(ptx) if err != nil { - log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) + log.Error("Failed to encode transaction for storage", "hash", ptx.Tx.Hash(), "err", err) return err } id, err := p.store.Put(blob) if err != nil { - log.Error("Failed to write transaction into storage", "hash", tx.Hash(), "err", err) + log.Error("Failed to write transaction into storage", "hash", ptx.Tx.Hash(), "err", err) return err } - - // Update the indices and metrics - meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx) + meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx) if _, ok := p.index[addr]; !ok { if err := p.reserver.Hold(addr); err != nil { - log.Warn("Failed to reserve account for blob pool", "tx", tx.Hash(), "from", addr, "err", err) + log.Warn("Failed to reserve account for blob pool", "tx", ptx.Tx.Hash(), "from", addr, "err", err) return err } p.index[addr] = []*blobTxMeta{meta} @@ -1404,20 +1559,32 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { if len(data) == 0 { return nil } - item := new(types.Transaction) - if err := rlp.DecodeBytes(data, item); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { id, _ := p.lookup.storeidOfTx(hash) log.Error("Blobs corrupted for traced transaction", "hash", hash, "id", id, "err", err) return nil } - return item + return ptx.ToTx() } -// GetRLP returns a RLP-encoded transaction if it is contained in the pool. +// GetRLP returns a RLP-encoded transaction for network if it is contained in the pool. +// It converts the pool's internal type to the RLP format used by the eth protocol: +// e.g. type_byte || [..., version, [blobs], [comms], [proofs]] func (p *BlobPool) GetRLP(hash common.Hash) []byte { - return p.getRLP(hash) + data := p.getRLP(hash) + if len(data) == 0 { + // Not in this pool, do not log. + return nil + } + rlp, err := encodeForNetwork(data) + if err != nil { + log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) + return nil + } + return rlp } // GetMetadata returns the transaction type and transaction size with the @@ -1486,18 +1653,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo } // Decode the blob transaction - tx := new(types.Transaction) - if err := rlp.DecodeBytes(data, tx); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err) continue } - sidecar := tx.BlobTxSidecar() - if sidecar == nil { - log.Error("Blob tx without sidecar", "hash", tx.Hash(), "id", txID) - continue - } + sidecar := ptx.Sidecar() // Traverse the blobs in the transaction - for i, hash := range tx.BlobHashes() { + for i, hash := range ptx.Tx.BlobHashes() { list, ok := indices[hash] if !ok { continue // non-interesting blob @@ -1517,7 +1680,8 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo case types.BlobSidecarVersion1: cellProofs, err := sidecar.CellProofsAt(i) if err != nil { - return nil, nil, nil, err + log.Error("Failed to get cell proofs", "id", txID, "err", err) + continue } pf = cellProofs } @@ -1596,9 +1760,10 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // Store the tx in memory, and revalidate later from, _ := types.Sender(p.signer, tx) allowance := p.gappedAllowance(from) - if allowance >= 1 && len(p.gapped) < maxGapped { + if allowance >= 1 && len(p.gappedSource) < maxGapped { p.gapped[from] = append(p.gapped[from], tx) p.gappedSource[tx.Hash()] = from + gappedGauge.Update(int64(len(p.gappedSource))) log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) return nil } else { @@ -1606,6 +1771,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // transactions by keeping the old and dropping this one. // Thus replacing a gapped transaction with another gapped transaction // is discouraged. + addGappedFullMeter.Mark(1) log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) } case errors.Is(err, core.ErrInsufficientFunds): @@ -1641,7 +1807,8 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error } // Transaction permitted into the pool from a nonce and cost perspective, // insert it into the database and update the indices - blob, err := rlp.EncodeToBytes(tx) + ptx := newBlobTxForPool(tx) + blob, err := rlp.EncodeToBytes(ptx) if err != nil { log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) return err @@ -1650,7 +1817,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error if err != nil { return err } - meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx) + meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), ptx) var ( next = p.state.GetNonce(from) @@ -1791,6 +1958,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // We do not recurse here, but continue to loop instead. // We are under lock, so we can add the transaction directly. if err := p.addLocked(tx, false); err == nil { + gappedPromotedMeter.Mark(1) log.Trace("Gapped blob transaction added to pool", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) } else { log.Trace("Gapped blob transaction not accepted", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "err", err) @@ -1802,6 +1970,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error } else { p.gapped[from] = gtxs } + gappedGauge.Update(int64(len(p.gappedSource))) } return nil } @@ -2069,8 +2238,9 @@ func (p *BlobPool) evictGapped() { keep = append(keep, gtx) } } - if len(keep) < len(txs) { - log.Trace("Evicting old gapped blob transactions", "count", len(txs)-len(keep), "from", from) + if evicted := len(txs) - len(keep); evicted > 0 { + gappedEvictedMeter.Mark(int64(evicted)) + log.Trace("Evicting old gapped blob transactions", "count", evicted, "from", from) } if len(keep) == 0 { delete(p.gapped, from) @@ -2078,6 +2248,7 @@ func (p *BlobPool) evictGapped() { p.gapped[from] = keep } } + gappedGauge.Update(int64(len(p.gappedSource))) } // isAnnouncable checks whether a transaction is announcable based on its diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 7c57755401..8032e21e8a 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -235,6 +235,12 @@ func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx) } +// encodeForPool encodes a blob transaction in the blobTxForPool storage format. +func encodeForPool(tx *types.Transaction) []byte { + blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx)) + return blob +} + // makeMultiBlobTx is a utility method to construct a ramdom blob tx with // certain number of blobs in its sidecar. func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction { @@ -530,7 +536,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5 tx := makeTx(nonce, 1, 1, 1, gapper) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce < 2 { @@ -547,7 +553,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling tx := makeTx(nonce, 1, 1, 1, dangler) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) dangling[id] = struct{}{} @@ -560,7 +566,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled tx := makeTx(nonce, 1, 1, 1, filler) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) filled[id] = struct{}{} @@ -573,7 +579,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled tx := makeTx(nonce, 1, 1, 1, overlapper) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce >= 2 { @@ -595,7 +601,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(uint64(i), 1, 1, 1, underpayer) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) underpaid[id] = struct{}{} @@ -614,7 +620,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(uint64(i), 1, 1, 1, outpricer) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if i < 2 { @@ -636,7 +642,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(nonce, 1, 1, 1, exceeder) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) exceeded[id] = struct{}{} @@ -654,7 +660,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(nonce, 1, 1, 1, overdrafter) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce < 1 { @@ -670,7 +676,7 @@ func TestOpenDrops(t *testing.T) { overcapped = make(map[uint64]struct{}) ) for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, overcapper)) + blob := encodeForPool(makeTx(nonce, 1, 1, 1, overcapper)) id, _ := store.Put(blob) if nonce < maxTxsPerAccount { @@ -686,7 +692,7 @@ func TestOpenDrops(t *testing.T) { duplicated = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, duplicater)) + blob := encodeForPool(makeTx(nonce, 1, 1, 1, duplicater)) for i := 0; i < int(nonce)+1; i++ { id, _ := store.Put(blob) @@ -705,7 +711,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2} { for i := 0; i < int(nonce)+1; i++ { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) + blob := encodeForPool(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) id, _ := store.Put(blob) if i == 0 { @@ -842,7 +848,7 @@ func TestOpenIndex(t *testing.T) { ) for _, i := range []int{5, 3, 4, 2, 0, 1} { // Randomize the tx insertion order to force sorting on load tx := makeTx(uint64(i), txExecTipCaps[i], txExecFeeCaps[i], txBlobFeeCaps[i], key) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) store.Put(blob) } store.Close() @@ -934,9 +940,9 @@ func TestOpenHeap(t *testing.T) { tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) heapOrder = []common.Address{addr2, addr1, addr3} heapIndex = map[common.Address]int{addr2: 0, addr1: 1, addr3: 2} @@ -1009,9 +1015,9 @@ func TestOpenCap(t *testing.T) { tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) keep = []common.Address{addr1, addr3} drop = []common.Address{addr2} @@ -1098,8 +1104,8 @@ func TestChangingSlotterSize(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) ) // Write the two safely sized txs to store. note: although the store is @@ -1201,8 +1207,8 @@ func TestBillyMigration(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) ) // Write the two safely sized txs to store. note: although the store is @@ -1281,6 +1287,85 @@ func TestBillyMigration(t *testing.T) { } } +// TestLegacyTxConversion verifies that on Init, transactions stored in the +// legacy *types.Transaction RLP format are detected and migrated into the new +// blobTxForPool storage format, and that they remain retrievable via the pool +// API after the conversion. +func TestLegacyTxConversion(t *testing.T) { + storage := t.TempDir() + os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) + os.MkdirAll(filepath.Join(storage, limboedTransactionStore), 0700) + + // Initialize the pending store with two blob transactions encoded in the + // legacy format. + queuedir := filepath.Join(storage, pendingTransactionStore) + store, err := billy.Open(billy.Options{Path: queuedir}, newSlotter(testMaxBlobsPerBlock), nil) + if err != nil { + t.Fatalf("failed to open billy: %v", err) + } + + key1, _ := crypto.GenerateKey() + key2, _ := crypto.GenerateKey() + addr1 := crypto.PubkeyToAddress(key1.PublicKey) + addr2 := crypto.PubkeyToAddress(key2.PublicKey) + + tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1, types.BlobSidecarVersion0) + tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2, types.BlobSidecarVersion0) + + for _, tx := range []*types.Transaction{tx1, tx2} { + legacy, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatalf("failed to legacy-encode tx: %v", err) + } + if _, err := store.Put(legacy); err != nil { + t.Fatalf("failed to put legacy blob: %v", err) + } + } + store.Close() + + // Init should migrate the legacy entries into the new storage format. + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) + statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) + statedb.Commit(0, true, false) + + chain := &testBlockChain{ + config: params.MainnetChainConfig, + basefee: uint256.NewInt(params.InitialBaseFee), + blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice), + statedb: statedb, + } + pool := New(Config{Datadir: storage}, chain, nil) + if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil { + t.Fatalf("failed to create blob pool: %v", err) + } + defer pool.Close() + + // Both transactions should be retrievable. + for _, want := range []*types.Transaction{tx1, tx2} { + got := pool.Get(want.Hash()) + if got == nil { + t.Fatalf("migrated tx %s not found in pool", want.Hash()) + } + if got.BlobTxSidecar() == nil { + t.Fatalf("migrated tx %s lost its sidecar", want.Hash()) + } + if got.Hash() != want.Hash() { + t.Fatalf("migrated tx hash mismatch: have %s, want %s", got.Hash(), want.Hash()) + } + } + + // Legacy formats should not exist on pool.store + pool.store.Iterate(func(id uint64, size uint32, blob []byte) { + var ptx blobTxForPool + if err := rlp.DecodeBytes(blob, &ptx); err != nil { + t.Errorf("entry %d not in new blobTxForPool format: %v", id, err) + } + }) + + verifyPoolInternals(t, pool) +} + // TestBlobCountLimit tests the blobpool enforced limits on the max blob count. func TestBlobCountLimit(t *testing.T) { var ( @@ -1746,7 +1831,7 @@ func TestAdd(t *testing.T) { // Sign the seed transactions and store them in the data store for _, tx := range seed.txs { signed := types.MustSignNewTx(keys[acc], types.LatestSigner(params.MainnetChainConfig), tx) - blob, _ := rlp.EncodeToBytes(signed) + blob := encodeForPool(signed) store.Put(blob) } } @@ -1853,9 +1938,9 @@ func TestGetBlobs(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion1) // [6, 12) tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3, types.BlobSidecarVersion0) // [12, 18) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) ) // Write the two safely sized txs to store. note: although the store is @@ -2055,6 +2140,32 @@ func TestGetBlobs(t *testing.T) { pool.Close() } +// TestEncodeForNetwork verifies that encodeForNetwork produces output identical +// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars. +func TestEncodeForNetwork(t *testing.T) { + t.Run("v0", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion0) }) + t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion1) }) +} + +func testEncodeForNetwork(t *testing.T, version byte) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, version) + + wantRLP, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatalf("failed to encode tx: %v", err) + } + storedRLP := encodeForPool(tx) + + gotRLP, err := encodeForNetwork(storedRLP) + if err != nil { + t.Fatalf("encodeForNetwork failed: %v", err) + } + if !bytes.Equal(gotRLP, wantRLP) { + t.Fatalf("network encoding mismatch (version %d): got %d bytes, want %d bytes", version, len(gotRLP), len(wantRLP)) + } +} + // fakeBilly is a billy.Database implementation which just drops data on the floor. type fakeBilly struct { billy.Database diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index 36284d6a03..b8bee2f22a 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -33,7 +33,7 @@ import ( type limboBlob struct { TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs Block uint64 // Block in which the blob transaction was included - Tx *types.Transaction + Ptx *blobTxForPool } // limbo is a light, indexed database to temporarily store recently included @@ -146,15 +146,14 @@ func (l *limbo) finalize(final *types.Header) { // push stores a new blob transaction into the limbo, waiting until finality for // it to be automatically evicted. -func (l *limbo) push(tx *types.Transaction, block uint64) error { - // If the blobs are already tracked by the limbo, consider it a programming - // error. There's not much to do against it, but be loud. - if _, ok := l.index[tx.Hash()]; ok { - log.Error("Limbo cannot push already tracked blobs", "tx", tx.Hash()) +func (l *limbo) push(ptx *blobTxForPool, block uint64) error { + hash := ptx.Tx.Hash() + if _, ok := l.index[hash]; ok { + log.Error("Limbo cannot push already tracked blobs", "tx", hash) return errors.New("already tracked blob transaction") } - if err := l.setAndIndex(tx, block); err != nil { - log.Error("Failed to set and index limboed blobs", "tx", tx.Hash(), "err", err) + if err := l.setAndIndex(ptx, block); err != nil { + log.Error("Failed to set and index limboed blobs", "tx", hash, "err", err) return err } return nil @@ -163,7 +162,7 @@ func (l *limbo) push(tx *types.Transaction, block uint64) error { // pull retrieves a previously pushed set of blobs back from the limbo, removing // it at the same time. This method should be used when a previously included blob // transaction gets reorged out. -func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) { +func (l *limbo) pull(tx common.Hash) (*blobTxForPool, error) { // If the blobs are not tracked by the limbo, there's not much to do. This // can happen for example if a blob transaction is mined without pushing it // into the network first. @@ -177,7 +176,7 @@ func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) { log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err) return nil, err } - return item.Tx, nil + return item.Ptx, nil } // update changes the block number under which a blob transaction is tracked. This @@ -209,7 +208,7 @@ func (l *limbo) update(txhash common.Hash, block uint64) { log.Error("Failed to get and drop limboed blobs", "tx", txhash, "id", id, "err", err) return } - if err := l.setAndIndex(item.Tx, block); err != nil { + if err := l.setAndIndex(item.Ptx, block); err != nil { log.Error("Failed to set and index limboed blobs", "tx", txhash, "err", err) return } @@ -240,12 +239,12 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) { // setAndIndex assembles a limbo blob database entry and stores it, also updating // the in-memory indices. -func (l *limbo) setAndIndex(tx *types.Transaction, block uint64) error { - txhash := tx.Hash() +func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error { + txhash := ptx.Tx.Hash() item := &limboBlob{ TxHash: txhash, Block: block, - Tx: tx, + Ptx: ptx, } data, err := rlp.EncodeToBytes(item) if err != nil { diff --git a/core/txpool/blobpool/metrics.go b/core/txpool/blobpool/metrics.go index 52419ade09..44e2098b22 100644 --- a/core/txpool/blobpool/metrics.go +++ b/core/txpool/blobpool/metrics.go @@ -97,9 +97,15 @@ var ( addUnderpricedMeter = metrics.NewRegisteredMeter("blobpool/add/underpriced", nil) // Gas tip too low, neutral addStaleMeter = metrics.NewRegisteredMeter("blobpool/add/stale", nil) // Nonce already filled, reject, bad-ish addGappedMeter = metrics.NewRegisteredMeter("blobpool/add/gapped", nil) // Nonce gapped, reject, bad-ish + addGappedFullMeter = metrics.NewRegisteredMeter("blobpool/add/gappedfull", nil) // Gapped queue full, reject, neutral addOverdraftedMeter = metrics.NewRegisteredMeter("blobpool/add/overdrafted", nil) // Balance exceeded, reject, neutral addOvercappedMeter = metrics.NewRegisteredMeter("blobpool/add/overcapped", nil) // Per-account cap exceeded, reject, neutral addNoreplaceMeter = metrics.NewRegisteredMeter("blobpool/add/noreplace", nil) // Replacement fees or tips too low, neutral addNonExclusiveMeter = metrics.NewRegisteredMeter("blobpool/add/nonexclusive", nil) // Plain transaction from same account exists, reject, neutral addValidMeter = metrics.NewRegisteredMeter("blobpool/add/valid", nil) // Valid transaction, add, neutral + + // Gapped queue metrics for observability + gappedGauge = metrics.NewRegisteredGauge("blobpool/gapped/count", nil) // Current gapped queue size + gappedPromotedMeter = metrics.NewRegisteredMeter("blobpool/gapped/promoted", nil) // Gapped txs successfully promoted to pool + gappedEvictedMeter = metrics.NewRegisteredMeter("blobpool/gapped/evicted", nil) // Gapped txs evicted due to timeout/stale ) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 00630de04c..3d66803fd7 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -467,8 +467,8 @@ func (pool *LegacyPool) stats() (int, int) { // Content retrieves the data content of the transaction pool, returning all the // pending as well as queued transactions, grouped by account and sorted by nonce. func (pool *LegacyPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { - pool.mu.Lock() - defer pool.mu.Unlock() + pool.mu.RLock() + defer pool.mu.RUnlock() pending := make(map[common.Address][]*types.Transaction, len(pool.pending)) for addr, list := range pool.pending { @@ -503,8 +503,8 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) (map[common.Address if filter.BlobTxs { return nil, 0 } - pool.mu.Lock() - defer pool.mu.Unlock() + pool.mu.RLock() + defer pool.mu.RUnlock() var count int pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending)) diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go index bb178f175e..66f3248105 100644 --- a/core/txpool/locals/tx_tracker.go +++ b/core/txpool/locals/tx_tracker.go @@ -18,6 +18,7 @@ package locals import ( + "cmp" "slices" "sync" "time" @@ -151,7 +152,7 @@ func (tracker *TxTracker) recheck(journalCheck bool) []*types.Transaction { for _, list := range rejournal { // cmp(a, b) should return a negative number when a < b, slices.SortFunc(list, func(a, b *types.Transaction) int { - return int(a.Nonce() - b.Nonce()) + return cmp.Compare(a.Nonce(), b.Nonce()) }) } // Rejournal the tracker while holding the lock. No new transactions will diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 6891dc94d2..c87bba31ac 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,7 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return err } @@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction can cover floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return err } diff --git a/core/types.go b/core/types.go index 87bbfcff58..edbfc43db3 100644 --- a/core/types.go +++ b/core/types.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" ) @@ -58,4 +59,8 @@ type ProcessResult struct { Requests [][]byte Logs []*types.Log GasUsed uint64 + + // BAL is only meaningful for post-Amsterdam blocks. Please ensure + // fork validation is performed before accessing it. + Bal *bal.ConstructionBlockAccessList } diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go deleted file mode 100644 index e563fa22e2..0000000000 --- a/core/types/bal/access_list.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see - -package bal - -import ( - "maps" - - "github.com/ethereum/go-ethereum/common" -) - -// StorageAccessList represents a set of storage slots accessed within an account. -type StorageAccessList map[common.Hash]struct{} - -// StateAccessList records the set of accounts and storage slots that have been -// accessed. An entry with an empty StorageAccessList denotes an account access -// without any storage slot access. -type StateAccessList struct { - list map[common.Address]StorageAccessList -} - -// NewStateAccessList returns an empty StateAccessList ready for use. -func NewStateAccessList() *StateAccessList { - return &StateAccessList{ - list: make(map[common.Address]StorageAccessList), - } -} - -// AddAccount records an access to the given account. It is a no-op if the -// account is already present. -func (s *StateAccessList) AddAccount(addr common.Address) { - if s == nil { - return - } - if _, exists := s.list[addr]; !exists { - s.list[addr] = make(StorageAccessList) - } -} - -// AddState records an access to the given storage slot. The owning account is -// implicitly recorded as well. -func (s *StateAccessList) AddState(addr common.Address, slot common.Hash) { - if s == nil { - return - } - slots, exists := s.list[addr] - if !exists { - slots = make(StorageAccessList) - s.list[addr] = slots - } - slots[slot] = struct{}{} -} - -// Merge merges the entries from other into the receiver. -func (s *StateAccessList) Merge(other *StateAccessList) { - if s == nil || other == nil { - return - } - for addr, otherSlots := range other.list { - slots, exists := s.list[addr] - if !exists { - s.list[addr] = otherSlots - continue - } - maps.Copy(slots, otherSlots) - } -} - -// Copy returns a deep copy of the StateAccessList. -func (s *StateAccessList) Copy() *StateAccessList { - if s == nil { - return nil - } - cpy := &StateAccessList{ - list: make(map[common.Address]StorageAccessList, len(s.list)), - } - for addr, slots := range s.list { - cpy.list[addr] = maps.Clone(slots) - } - return cpy -} diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 99ead8d6f0..2eb5fe93cd 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -71,8 +71,8 @@ type ConstructionBlockAccessList struct { } // NewConstructionBlockAccessList instantiates an empty access list. -func NewConstructionBlockAccessList() ConstructionBlockAccessList { - return ConstructionBlockAccessList{ +func NewConstructionBlockAccessList() *ConstructionBlockAccessList { + return &ConstructionBlockAccessList{ Accounts: make(map[common.Address]*ConstructionAccountAccess), } } @@ -138,10 +138,62 @@ func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common // PrettyPrint returns a human-readable representation of the access list func (b *ConstructionBlockAccessList) PrettyPrint() string { - enc := b.toEncodingObj() + enc := b.ToEncodingObj() return enc.PrettyPrint() } +// Merge applies other on top of the local block access list. For colliding +// entries (a (slot, txIdx) write or a txIdx-keyed balance/nonce/code change), +// the value from other wins, matching the semantics of applying the local +// effects first and then other's. Storage reads are unioned; any slot +// written by either side is dropped from StorageReads. +// +// Typically each list covers its own tx index, so txIdx-level collisions are +// not expected; the exception is pre/post-transition system calls, which +// share a single tx index. In that case callers must pass block-accessList +// in order strictly. +// +// other is referenced (not deep copied), after the call both lists share +// inner maps and other must not be mutated. +func (b *ConstructionBlockAccessList) Merge(other *ConstructionBlockAccessList) { + if other == nil { + return + } + for addr, otherAcc := range other.Accounts { + acc, ok := b.Accounts[addr] + if !ok { + b.Accounts[addr] = otherAcc + continue + } + for key, writes := range otherAcc.StorageWrites { + existing, ok := acc.StorageWrites[key] + if !ok { + acc.StorageWrites[key] = writes + } else { + for txIdx, value := range writes { + existing[txIdx] = value + } + } + delete(acc.StorageReads, key) + } + for key := range otherAcc.StorageReads { + if _, ok := acc.StorageWrites[key]; ok { + continue + } + acc.StorageReads[key] = struct{}{} + } + for txIdx, balance := range otherAcc.BalanceChanges { + acc.BalanceChanges[txIdx] = balance + } + for txIdx, nonce := range otherAcc.NonceChanges { + acc.NonceChanges[txIdx] = nonce + } + for txIdx, code := range otherAcc.CodeChange { + acc.CodeChange[txIdx] = code + } + } +} + // Copy returns a deep copy of the access list. func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { res := NewConstructionBlockAccessList() @@ -169,5 +221,5 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { aaCopy.CodeChange = codes res.Accounts[addr] = &aaCopy } - return &res + return res } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 03f97f3809..399f9db7c0 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -78,17 +78,43 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate(rules params.Rules) error { +func (e *BlockAccessList) Validate(blockGasLimit uint64) error { if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } for _, entry := range *e { - if err := entry.validate(rules); err != nil { + if err := entry.validate(); err != nil { return err } } + return e.ValidateSize(blockGasLimit) +} + +// itemCount returns the number of items in the BAL for EIP-7928 size-constraint +// purposes: the count of distinct addresses plus every storage key (writes + +// reads) carried by those accounts. A storage slot is counted once regardless +// of how many transactions wrote to it. +func (e *BlockAccessList) itemCount() uint64 { + count := uint64(len(*e)) // distinct addresses + for i := range *e { + count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads)) + } + return count +} + +// ValidateSize returns an error if the BAL violates the EIP-7928 size +// constraint for the given block gas limit: +// +// itemCount() <= blockGasLimit / params.BALItemCost +func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error { + items := e.itemCount() + limit := blockGasLimit / params.BALItemCost + if items > limit { + return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)", + items, limit, blockGasLimit, params.BALItemCost) + } return nil } @@ -159,7 +185,7 @@ type AccountAccess struct { // validate converts the account accesses out of encoding format. // If any of the keys in the encoding object are not ordered according to the // spec, an error is returned. -func (e *AccountAccess) validate(rules params.Rules) error { +func (e *AccountAccess) validate() error { // Check the storage write slots are sorted in order if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { return a.Slot.Cmp(b.Slot) @@ -200,14 +226,7 @@ func (e *AccountAccess) validate(rules params.Rules) error { return errors.New("code changes not in ascending order by tx index") } for _, change := range e.CodeChanges { - var sizeLimit int - switch { - case rules.IsAmsterdam: - sizeLimit = params.MaxCodeSizeAmsterdam - default: - sizeLimit = params.MaxCodeSize - } - if len(change.Code) > sizeLimit { + if len(change.Code) > params.MaxCodeSizeAmsterdam { return errors.New("code change contained oversized code") } } @@ -257,7 +276,7 @@ func (e *AccountAccess) Copy() AccountAccess { // EncodeRLP returns the RLP-encoded access list func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { - return b.toEncodingObj().EncodeRLP(wr) + return b.ToEncodingObj().EncodeRLP(wr) } var _ rlp.Encoder = &ConstructionBlockAccessList{} @@ -340,9 +359,9 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc return res } -// toEncodingObj returns an instance of the access list expressed as the type +// ToEncodingObj returns an instance of the access list expressed as the type // which is used as input for the encoding/decoding. -func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList { +func (b *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { var addresses []common.Address for addr := range b.Accounts { addresses = append(addresses, addr) diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 32a0292f2e..2b6a3c194e 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -19,6 +19,7 @@ package bal import ( "bytes" "cmp" + "math" "reflect" "slices" "testing" @@ -98,14 +99,65 @@ func TestBALEncoding(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("decoding failed: %v\n", err) } - if dec.Hash() != bal.toEncodingObj().Hash() { + if dec.Hash() != bal.ToEncodingObj().Hash() { t.Fatalf("encoded block hash doesn't match decoded") } - if !reflect.DeepEqual(bal.toEncodingObj(), &dec) { + if !reflect.DeepEqual(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } +func TestConstructionBALMerge(t *testing.T) { + var ( + addrA = common.BytesToAddress([]byte{0xAA}) + addrB = common.BytesToAddress([]byte{0xBB}) + slot1 = common.BytesToHash([]byte{0x01}) + slot2 = common.BytesToHash([]byte{0x02}) + slot3 = common.BytesToHash([]byte{0x03}) + ) + a := NewConstructionBlockAccessList() + a.StorageWrite(1, addrA, slot1, common.BytesToHash([]byte{0x11})) + a.StorageRead(addrA, slot2) // demoted by other's write below + a.BalanceChange(1, addrA, uint256.NewInt(100)) + a.NonceChange(addrA, 1, 7) + + b := NewConstructionBlockAccessList() + b.StorageWrite(2, addrA, slot1, common.BytesToHash([]byte{0x22})) // same slot, disjoint txIdx + b.StorageWrite(2, addrA, slot2, common.BytesToHash([]byte{0x33})) + b.StorageRead(addrA, slot3) + b.BalanceChange(2, addrA, uint256.NewInt(200)) + b.NonceChange(addrA, 2, 8) + b.CodeChange(addrB, 2, []byte{0xde, 0xad}) // account only in other + + a.Merge(b) + + accA := a.Accounts[addrA] + wantWrites := map[common.Hash]map[uint32]common.Hash{ + slot1: {1: common.BytesToHash([]byte{0x11}), 2: common.BytesToHash([]byte{0x22})}, + slot2: {2: common.BytesToHash([]byte{0x33})}, + } + if !reflect.DeepEqual(accA.StorageWrites, wantWrites) { + t.Fatalf("storage writes mismatch: got %v, want %v", accA.StorageWrites, wantWrites) + } + wantReads := map[common.Hash]struct{}{slot3: {}} + if !reflect.DeepEqual(accA.StorageReads, wantReads) { + t.Fatalf("storage reads mismatch: got %v, want %v", accA.StorageReads, wantReads) + } + if accA.BalanceChanges[1].Uint64() != 100 || accA.BalanceChanges[2].Uint64() != 200 { + t.Fatalf("balance changes mismatch: %v", accA.BalanceChanges) + } + if accA.NonceChanges[1] != 7 || accA.NonceChanges[2] != 8 { + t.Fatalf("nonce changes mismatch: %v", accA.NonceChanges) + } + accB, ok := a.Accounts[addrB] + if !ok { + t.Fatal("account only present in other was not adopted") + } + if !bytes.Equal(accB.CodeChange[2], []byte{0xde, 0xad}) { + t.Fatalf("code change for adopted account missing: %x", accB.CodeChange[2]) + } +} + func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites @@ -231,10 +283,82 @@ func TestBlockAccessListCopy(t *testing.T) { } } +func TestBlockAccessListItemCount(t *testing.T) { + empty := &BlockAccessList{} + if got := empty.itemCount(); got != 0 { + t.Fatalf("empty BAL item count: got %d, want 0", got) + } + + addr1 := [20]byte(testrand.Bytes(20)) + addr2 := [20]byte(testrand.Bytes(20)) + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := &BlockAccessList{ + AccountAccess{ + Address: addr1, + StorageWrites: []encodingSlotWrites{ + {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}}, + {Slot: one()}, + }, + StorageReads: []*uint256.Int{one()}, + }, + AccountAccess{Address: addr2}, // address-only, no slots + } + // 2 addresses + 2 write-slots + 1 read-slot = 5 items. + // (Multiple TxIdx writes to the same slot count as ONE item.) + if got := bal.itemCount(); got != 5 { + t.Fatalf("item count: got %d, want 5", got) + } +} + +func TestBlockAccessListValidateSize(t *testing.T) { + // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage + // slots (some writes, some reads). 3 + 9*3 = 30. + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := make(BlockAccessList, 3) + for i := range bal { + bal[i].Address = [20]byte(testrand.Bytes(20)) + for j := 0; j < 5; j++ { + bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{ + Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}}, + }) + } + for j := 0; j < 4; j++ { + bal[i].StorageReads = append(bal[i].StorageReads, one()) + } + } + if got := bal.itemCount(); got != 30 { + t.Fatalf("setup: item count = %d, want 30", got) + } + + // limit = blockGasLimit / BALItemCost. + // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000. + tests := []struct { + name string + gasLimit uint64 + expectError bool + }{ + {"exactly at limit", 30 * params.BALItemCost, false}, + {"well above limit", 60_000_000, false}, + {"one below limit", 30*params.BALItemCost - 1, true}, + {"zero gas limit", 0, true}, + } + for _, tc := range tests { + err := bal.ValidateSize(tc.gasLimit) + if (err != nil) != tc.expectError { + t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError) + } + } + + // Empty BAL is always valid (even with 0 gas limit). + if err := (&BlockAccessList{}).ValidateSize(0); err != nil { + t.Fatalf("empty BAL must pass any limit: %v", err) + } +} + func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) - if err := enc.Validate(params.Rules{}); err != nil { + if err := enc.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -246,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(params.Rules{}); err != nil { + if err := dec.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() - if err := listB.Validate(params.Rules{}); err != nil { + listB := cBAL.ToEncodingObj() + if err := listB.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } diff --git a/core/types/block.go b/core/types/block.go index ea576ed232..0856845a4e 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -413,8 +413,9 @@ func (b *Block) BaseFee() *big.Int { return new(big.Int).Set(b.header.BaseFee) } -func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } -func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash } func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 diff --git a/core/types/hashes.go b/core/types/hashes.go index db8912a66f..541681e4db 100644 --- a/core/types/hashes.go +++ b/core/types/hashes.go @@ -43,6 +43,9 @@ var ( // EmptyRequestsHash is the known hash of an empty request set, sha256(""). EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + // EmptyBlockAccessListHash is the known hash of an empty block accessList, keccak256(rlp.encode([])). + EmptyBlockAccessListHash = common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + // EmptyBinaryHash is the known hash of an empty binary trie. EmptyBinaryHash = common.Hash{} ) diff --git a/core/vm/contract.go b/core/vm/contract.go index a55a5dde8b..45c879c80f 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -131,8 +131,8 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G if !ok { return false } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(prior, c.Gas.RegularGas, reason) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } return true } @@ -143,8 +143,8 @@ func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tra if !changed { return } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(prior, c.Gas.RegularGas, reason) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 6dadb64873..71cfdbc527 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -269,8 +269,8 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm gas.Exhaust() return nil, gas, ErrOutOfGas } - if logger != nil && logger.OnGasChange != nil { - logger.OnGasChange(prior, gas.RegularGas, tracing.GasChangeCallPrecompiledContract) + if logger.HasGasHook() { + logger.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeCallPrecompiledContract) } // Touch the precompile for block-level accessList recording once Amsterdam // fork is activated. diff --git a/core/vm/evm.go b/core/vm/evm.go index 26b2f73a00..832306b9a0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -317,8 +317,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -371,8 +371,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -415,8 +415,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -470,8 +470,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -509,8 +509,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value gas.Exhaust() return nil, common.Address{}, gas, ErrOutOfGas } - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractCollisionCheck) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) } } @@ -528,8 +528,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() return nil, common.Address{}, gas, ErrContractAddressCollision @@ -558,8 +558,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value return nil, common.Address{}, gas, ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractInit) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractInit) } } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) @@ -673,15 +673,17 @@ func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to comm if tracer.OnEnter != nil { tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) } - if tracer.OnGasChange != nil { - tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance) + if tracer.HasGasHook() { + initial := NewGasBudget(startGas) + tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance) } } func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) { tracer := evm.Config.Tracer - if leftOverGas != 0 && tracer.OnGasChange != nil { - tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned) + if leftOverGas != 0 && tracer.HasGasHook() { + leftover := NewGasBudget(leftOverGas) + tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) } var reverted bool if err != nil { @@ -707,3 +709,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext { StateDB: evm.StateDB, } } + +// GetRules returns the chain rules used throughout the EVM execution. +func (evm *EVM) GetRules() params.Rules { + return evm.chainRules +} diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index cc90c54798..ed938ae41f 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -16,7 +16,11 @@ package vm -import "fmt" +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core/tracing" +) // GasCosts denotes a vector of gas costs in the // multidimensional metering paradigm. It represents the cost @@ -77,21 +81,26 @@ func (g GasBudget) CanAfford(cost GasCosts) bool { } // Charge deducts the given gas cost from the budget. It returns the -// pre-charge gas value and false if the budget does not have sufficient +// pre-charge budget and false if the budget does not have sufficient // gas to cover the cost. -func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { - prior := g.RegularGas - if prior < cost.RegularGas { +func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { + prior := *g + if g.RegularGas < cost.RegularGas { return prior, false } g.RegularGas -= cost.RegularGas return prior, true } -// Refund adds the given gas budget back. It returns the pre-refund gas -// value and whether the budget was actually changed. -func (g *GasBudget) Refund(other GasBudget) (uint64, bool) { - prior := g.RegularGas +// Refund adds the given gas budget back. It returns the pre-refund budget +// and whether the budget was actually changed. +func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) { + prior := *g g.RegularGas += other.RegularGas - return prior, g.RegularGas != prior + return prior, g.RegularGas != prior.RegularGas +} + +// AsTracing converts the GasBudget into the tracing-facing Gas vector. +func (g GasBudget) AsTracing() tracing.Gas { + return tracing.Gas{Regular: g.RegularGas, State: g.StateGas} } diff --git a/core/vm/interface.go b/core/vm/interface.go index 487d8002f9..a9938c2a28 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -98,5 +98,6 @@ type StateDB interface { AccessEvents() *state.AccessEvents // Finalise must be invoked at the end of a transaction - Finalise(bool) *bal.StateAccessList + Finalise(bool) *bal.ConstructionBlockAccessList + SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 4c278fc857..3994327247 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -234,8 +234,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // Do tracing before potential memory expansion if debug { - if evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange( + tracing.Gas{Regular: gasCopy, State: contract.Gas.StateGas}, + tracing.Gas{Regular: gasCopy - cost, State: contract.Gas.StateGas}, + tracing.GasChangeCallOpCode, + ) } if evm.Config.Tracer.OnOpcode != nil { evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) diff --git a/crypto/signature_nocgo.go b/crypto/signature_nocgo.go index 0aab7180d3..bf273612e9 100644 --- a/crypto/signature_nocgo.go +++ b/crypto/signature_nocgo.go @@ -103,7 +103,7 @@ func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) { // The public key should be in compressed (33 bytes) or uncompressed (65 bytes) format. // The signature should have the 64 byte [R || S] format. func VerifySignature(pubkey, hash, signature []byte) bool { - if len(signature) != 64 { + if len(signature) != 64 || len(hash) != DigestLength { return false } var r, s secp256k1.ModNScalar diff --git a/eth/api_backend.go b/eth/api_backend.go index 33fe4fe5d9..5e3558d8eb 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/filtermaps" @@ -61,9 +62,9 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header { return b.eth.blockchain.CurrentBlock() } -func (b *EthAPIBackend) SetHead(number uint64) { +func (b *EthAPIBackend) SetHead(number uint64) error { b.eth.handler.downloader.Cancel() - b.eth.blockchain.SetHead(number) + return b.eth.blockchain.SetHead(number) } func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { @@ -430,6 +431,13 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) } +func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { + if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { + return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + } + return nil +} + func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { if excess := b.CurrentHeader().ExcessBlobGas; excess != nil { return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader()) diff --git a/eth/backend.go b/eth/backend.go index 08a3c70c9d..af8b04bda6 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -49,7 +49,6 @@ import ( "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/shutdowncheck" "github.com/ethereum/go-ethereum/internal/version" @@ -105,7 +104,6 @@ type Ethereum struct { // DB interfaces chainDb ethdb.Database // Block chain database - eventMux *event.TypeMux engine consensus.Engine accountManager *accounts.Manager @@ -194,7 +192,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth := &Ethereum{ config: config, chainDb: chainDb, - eventMux: stack.EventMux(), accountManager: stack.AccountManager(), engine: engine, networkID: networkID, @@ -237,6 +234,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { StateHistory: config.StateHistory, TrienodeHistory: config.TrienodeHistory, NodeFullValueCheckpoint: config.NodeFullValueCheckpoint, + BinTrieGroupDepth: config.BinTrieGroupDepth, StateScheme: scheme, HistoryPolicy: histPolicy, TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), @@ -343,7 +341,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { Network: networkID, Sync: config.SyncMode, BloomCache: uint64(cacheLimit), - EventMux: eth.eventMux, RequiredBlocks: config.RequiredBlocks, }); err != nil { return nil, err @@ -404,7 +401,7 @@ func (s *Ethereum) APIs() []rpc.API { Service: NewMinerAPI(s), }, { Namespace: "eth", - Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain, s.eventMux), + Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain), }, { Namespace: "admin", Service: NewAdminAPI(s), @@ -599,7 +596,6 @@ func (s *Ethereum) Stop() error { s.shutdownTracker.Stop() s.chainDb.Close() - s.eventMux.Stop() return nil } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index be19bd5de4..3861f62813 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -82,6 +82,9 @@ const ( // beaconUpdateWarnFrequency is the frequency at which to warn the user that // the beacon client is offline. beaconUpdateWarnFrequency = 5 * time.Minute + + // maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated. + maxReorgDepth = 32 ) type ConsensusAPI struct { @@ -237,6 +240,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine. func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes, payloadVersion engine.PayloadVersion, payloadWitness bool) (result engine.ForkChoiceResponse, err error) { ctx, _, spanEnd := telemetry.StartSpan(ctx, "engine.forkchoiceUpdated") defer spanEnd(&err) + api.forkchoiceLock.Lock() defer api.forkchoiceLock.Unlock() @@ -321,10 +325,23 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo // generating the payload. It's a special corner case that a few slots are // missing and we are requested to generate the payload in slot. } else { - // If the head block is already in our canonical chain, the beacon client is - // probably resyncing. Ignore the update. - log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) - return valid(nil), nil + if finalized := api.eth.BlockChain().CurrentFinalBlock(); finalized != nil && block.NumberU64() <= finalized.Number.Uint64() { + log.Info("Skipping beacon update to finalized ancestor", "number", block.NumberU64(), "hash", update.HeadBlockHash) + return valid(nil), nil + } + depth := api.eth.BlockChain().CurrentBlock().Number.Uint64() - block.NumberU64() + if depth >= maxReorgDepth { + log.Warn("Refusing too deep reorg", "depth", depth, "head", update.HeadBlockHash) + return engine.STATUS_INVALID, engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds limit %d", depth, maxReorgDepth)) + } + if !api.eth.Synced() { + log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash) + return valid(nil), nil + } + if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { + log.Error("Error setting canonical", "number", block.NumberU64(), "hash", update.HeadBlockHash, "error", err) + return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err + } } api.eth.SetSynced() @@ -629,6 +646,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd return nil, engine.InvalidParams.With(err) } // Validate the blobs from the pool and assemble the response + filled := 0 res := make(engine.BlobAndProofListV2, len(hashes)) for i := range blobs { // The blob has been evicted since the last AvailableBlobs call. @@ -649,10 +667,11 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd Blob: blobs[i][:], CellProofs: cellProofs, } + filled++ } - if len(res) == len(hashes) { + if filled == len(hashes) { getBlobsRequestCompleteHit.Inc(1) - } else if len(res) > 0 { + } else if filled > 0 { getBlobsRequestPartialHit.Inc(1) } else { getBlobsRequestMiss.Inc(1) diff --git a/eth/downloader/api.go b/eth/downloader/api.go index 1fea35775e..6033e44474 100644 --- a/eth/downloader/api.go +++ b/eth/downloader/api.go @@ -23,7 +23,6 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/rpc" ) @@ -33,20 +32,18 @@ import ( type DownloaderAPI struct { d *Downloader chain *core.BlockChain - mux *event.TypeMux installSyncSubscription chan chan interface{} uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest } // NewDownloaderAPI creates a new DownloaderAPI. The API has an internal event loop that -// listens for events from the downloader through the global event mux. In case it receives one of +// listens for events from the downloader through the event feed. In case it receives one of // these events it broadcasts it to all syncing subscriptions that are installed through the // installSyncSubscription channel. -func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) *DownloaderAPI { +func NewDownloaderAPI(d *Downloader, chain *core.BlockChain) *DownloaderAPI { api := &DownloaderAPI{ d: d, chain: chain, - mux: m, installSyncSubscription: make(chan chan interface{}), uninstallSyncSubscription: make(chan *uninstallSyncSubscriptionRequest), } @@ -66,7 +63,8 @@ func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) * // receive is {false}. func (api *DownloaderAPI) eventLoop() { var ( - sub = api.mux.Subscribe(StartEvent{}) + events = make(chan SyncEvent, 16) + sub = api.d.SubscribeSyncEvents(events) syncSubscriptions = make(map[chan interface{}]struct{}) checkInterval = time.Second * 60 checkTimer = time.NewTimer(checkInterval) @@ -90,6 +88,7 @@ func (api *DownloaderAPI) eventLoop() { } ) defer checkTimer.Stop() + defer sub.Unsubscribe() for { select { @@ -101,14 +100,13 @@ func (api *DownloaderAPI) eventLoop() { case u := <-api.uninstallSyncSubscription: delete(syncSubscriptions, u.c) close(u.uninstalled) - case event := <-sub.Chan(): - if event == nil { - return - } - switch event.Data.(type) { - case StartEvent: + case ev := <-events: + if ev.Type == SyncStarted { started = true } + case <-sub.Err(): + // The downloader is terminated or other internal error occurs + return case <-checkTimer.C: if !started { checkTimer.Reset(checkInterval) diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 1de0933842..4a575d6856 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -97,9 +97,12 @@ type headerTask struct { } type Downloader struct { - mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode - moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle - mux *event.TypeMux // Event multiplexer to announce sync operation events + mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode + moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle + + // Event feed for downloader events + feed event.FeedOf[SyncEvent] + scope event.SubscriptionScope queue *queue // Scheduler for selecting the hashes to download peers *peerSet // Set of active peers from which download can proceed @@ -229,12 +232,11 @@ type BlockChain interface { } // New creates a new downloader to fetch hashes and blocks from remote peers. -func New(stateDb ethdb.Database, mode ethconfig.SyncMode, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader { +func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader { cutoffNumber, cutoffHash := chain.HistoryPruningCutoff() dl := &Downloader{ stateDB: stateDb, moder: newSyncModer(mode, chain, stateDb), - mux: mux, queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), peers: newPeerSet(), blockchain: chain, @@ -427,20 +429,25 @@ func (d *Downloader) ConfigSyncMode() SyncMode { return d.moder.get(false) } +// SubscribeSyncEvents creates a subscription for downloader sync events +func (d *Downloader) SubscribeSyncEvents(ch chan<- SyncEvent) event.Subscription { + return d.scope.Track(d.feed.Subscribe(ch)) +} + // syncToHead starts a block synchronization based on the hash chain from // the specified head hash. func (d *Downloader) syncToHead() (err error) { - d.mux.Post(StartEvent{}) + mode := d.getMode() + d.feed.Send(SyncEvent{Type: SyncStarted, Mode: mode}) defer func() { // reset on error if err != nil { - d.mux.Post(FailedEvent{err}) + d.feed.Send(SyncEvent{Type: SyncFailed, Mode: mode, Err: err}) } else { latest := d.blockchain.CurrentHeader() - d.mux.Post(DoneEvent{latest}) + d.feed.Send(SyncEvent{Type: SyncCompleted, Mode: mode, Latest: latest}) } }() - mode := d.getMode() log.Debug("Backfilling with the network", "mode", mode) defer func(start time.Time) { @@ -662,6 +669,9 @@ func (d *Downloader) Cancel() { // Terminate interrupts the downloader, canceling all pending operations. // The downloader cannot be reused after calling Terminate. func (d *Downloader) Terminate() { + // Unsubscribe all subscriptions registered from downloader + d.scope.Close() + // Close the termination channel (make sure double close is allowed) d.quitLock.Lock() select { diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 6d5d159631..e6c477cd33 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -75,7 +74,7 @@ func newTesterWithNotification(t *testing.T, mode ethconfig.SyncMode, success fu chain: chain, peers: make(map[string]*downloadTesterPeer), } - tester.downloader = New(db, mode, new(event.TypeMux), tester.chain, tester.dropPeer, success) + tester.downloader = New(db, mode, tester.chain, tester.dropPeer, success) return tester } diff --git a/eth/downloader/events.go b/eth/downloader/events.go index 25255a3a72..0fb380a857 100644 --- a/eth/downloader/events.go +++ b/eth/downloader/events.go @@ -16,10 +16,24 @@ package downloader -import "github.com/ethereum/go-ethereum/core/types" +import ( + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" +) -type DoneEvent struct { - Latest *types.Header +// SyncEventType represents the type of sync event +type SyncEventType int + +const ( + SyncStarted SyncEventType = iota + SyncFailed + SyncCompleted +) + +// SyncEvent represents a downloader synchronization event +type SyncEvent struct { + Type SyncEventType + Mode ethconfig.SyncMode + Err error // Set when Type is SyncFailed + Latest *types.Header // Set when Type is SyncCompleted } -type StartEvent struct{} -type FailedEvent struct{ Err error } diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index dd17b7f1ed..585906b8bd 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -689,9 +689,9 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, i++ } - for _, header := range request.Headers[:i] { + for k, header := range request.Headers[:i] { if res, stale, err := q.resultCache.GetDeliverySlot(header.Number.Uint64()); err == nil && !stale { - reconstruct(accepted, res) + reconstruct(k, res) accepted++ } else { // Between here and above, some other peer filled this result, diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index dd7436bf52..b51b78e199 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/pathdb" ) @@ -59,6 +60,7 @@ var Defaults = Config{ StateHistory: pathdb.Defaults.StateHistory, TrienodeHistory: pathdb.Defaults.TrienodeHistory, NodeFullValueCheckpoint: pathdb.Defaults.FullValueCheckpoint, + BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth, DatabaseCache: 2048, TrieCleanCache: 614, TrieDirtyCache: 1024, @@ -125,6 +127,11 @@ type Config struct { // consistent with persistent state. StateScheme string `toml:",omitempty"` + // BinTrieGroupDepth is the number of levels per serialized group in binary trie. + // Valid values are 1-8, with 8 being the default (byte-aligned groups). + // Lower values create smaller groups with more nodes. + BinTrieGroupDepth int `toml:",omitempty"` + // RequiredBlocks is a set of block number -> hash mappings which must be in the // canonical chain of all remote peers. Setting the option makes geth verify the // presence of these blocks for every new peer connection. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index ed85562f44..c5e45348be 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -34,6 +34,7 @@ func (c Config) MarshalTOML() (interface{}, error) { TrienodeHistory int64 `toml:",omitempty"` NodeFullValueCheckpoint uint32 `toml:",omitempty"` StateScheme string `toml:",omitempty"` + BinTrieGroupDepth int `toml:",omitempty"` RequiredBlocks map[uint64]common.Hash `toml:"-"` SlowBlockThreshold time.Duration `toml:",omitempty"` SkipBcVersionCheck bool `toml:"-"` @@ -87,6 +88,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.TrienodeHistory = c.TrienodeHistory enc.NodeFullValueCheckpoint = c.NodeFullValueCheckpoint enc.StateScheme = c.StateScheme + enc.BinTrieGroupDepth = c.BinTrieGroupDepth enc.RequiredBlocks = c.RequiredBlocks enc.SlowBlockThreshold = c.SlowBlockThreshold enc.SkipBcVersionCheck = c.SkipBcVersionCheck @@ -144,6 +146,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { TrienodeHistory *int64 `toml:",omitempty"` NodeFullValueCheckpoint *uint32 `toml:",omitempty"` StateScheme *string `toml:",omitempty"` + BinTrieGroupDepth *int `toml:",omitempty"` RequiredBlocks map[uint64]common.Hash `toml:"-"` SlowBlockThreshold *time.Duration `toml:",omitempty"` SkipBcVersionCheck *bool `toml:"-"` @@ -234,6 +237,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.StateScheme != nil { c.StateScheme = *dec.StateScheme } + if dec.BinTrieGroupDepth != nil { + c.BinTrieGroupDepth = *dec.BinTrieGroupDepth + } if dec.RequiredBlocks != nil { c.RequiredBlocks = dec.RequiredBlocks } diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index ace0752037..f45fc0d8c9 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -22,13 +22,13 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) // Options are the contextual parameters to execute the requested call. @@ -70,17 +70,17 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin } // Normalize the max fee per gas the call is willing to spend. - var feeCap *big.Int + var feeCap *uint256.Int if call.GasFeeCap != nil { feeCap = call.GasFeeCap } else if call.GasPrice != nil { feeCap = call.GasPrice } else { - feeCap = common.Big0 + feeCap = uint256.NewInt(0) } // Recap the highest gas limit with account's available balance. if feeCap.BitLen() != 0 { - balance := opts.State.GetBalance(call.From).ToBig() + balance := opts.State.GetBalance(call.From).Clone() available := balance if call.Value != nil { @@ -90,8 +90,8 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin available.Sub(available, call.Value) } if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 { - blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob) - blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes))) + blobGasPerBlob := uint256.NewInt(params.BlobTxBlobGasPerBlob) + blobBalanceUsage := uint256.NewInt(uint64(len(call.BlobHashes))) blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob) blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap) if blobBalanceUsage.Cmp(available) >= 0 { @@ -99,13 +99,13 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin } available.Sub(available, blobBalanceUsage) } - allowance := new(big.Int).Div(available, feeCap) + allowance := new(uint256.Int).Div(available, feeCap) // If the allowance is larger than maximum uint64, skip checking if allowance.IsUint64() && hi > allowance.Uint64() { transfer := call.Value if transfer == nil { - transfer = new(big.Int) + transfer = new(uint256.Int) } log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, "sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance) diff --git a/eth/handler.go b/eth/handler.go index 27b5e60697..76df635fb0 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -107,7 +107,6 @@ type handlerConfig struct { Network uint64 // Network identifier to advertise Sync ethconfig.SyncMode // Whether to snap or full sync BloomCache uint64 // Megabytes to alloc for snap sync bloom - EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges } @@ -126,7 +125,6 @@ type handler struct { peers *peerSet txBroadcastKey [16]byte - eventMux *event.TypeMux txsCh chan core.NewTxsEvent txsSub event.Subscription blockRange *blockRangeState @@ -144,14 +142,9 @@ type handler struct { // newHandler returns a handler for all Ethereum chain management protocol. func newHandler(config *handlerConfig) (*handler, error) { - // Create the protocol manager with the base fields - if config.EventMux == nil { - config.EventMux = new(event.TypeMux) // Nicety initialization for tests - } h := &handler{ nodeID: config.NodeID, networkID: config.Network, - eventMux: config.EventMux, database: config.Database, txpool: config.TxPool, chain: config.Chain, @@ -163,7 +156,7 @@ func newHandler(config *handlerConfig) (*handler, error) { handlerStartCh: make(chan struct{}), } // Construct the downloader (long sync) - h.downloader = downloader.New(config.Database, config.Sync, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures) + h.downloader = downloader.New(config.Database, config.Sync, h.chain, h.removePeer, h.enableSyncedFeatures) // If snap sync is requested but snapshots are disabled, fail loudly if h.downloader.ConfigSyncMode() == ethconfig.SnapSync && (config.Chain.Snapshots() == nil && config.Chain.TrieDB().Scheme() == rawdb.HashScheme) { @@ -420,7 +413,7 @@ func (h *handler) Start(maxPeers int) { // broadcast block range h.wg.Add(1) - h.blockRange = newBlockRangeState(h.chain, h.eventMux) + h.blockRange = newBlockRangeState(h.chain, h.downloader) go h.blockRangeLoop(h.blockRange) // start sync handlers @@ -536,16 +529,19 @@ type blockRangeState struct { next atomic.Pointer[eth.BlockRangeUpdatePacket] headCh chan core.ChainHeadEvent headSub event.Subscription - syncSub *event.TypeMuxSubscription + syncCh chan downloader.SyncEvent + syncSub event.Subscription } -func newBlockRangeState(chain *core.BlockChain, typeMux *event.TypeMux) *blockRangeState { +func newBlockRangeState(chain *core.BlockChain, dl *downloader.Downloader) *blockRangeState { headCh := make(chan core.ChainHeadEvent, chainHeadChanSize) headSub := chain.SubscribeChainHeadEvent(headCh) - syncSub := typeMux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{}) + syncCh := make(chan downloader.SyncEvent, 16) + syncSub := dl.SubscribeSyncEvents(syncCh) st := &blockRangeState{ headCh: headCh, headSub: headSub, + syncCh: syncCh, syncSub: syncSub, } st.update(chain, chain.CurrentBlock()) @@ -561,11 +557,8 @@ func (h *handler) blockRangeLoop(st *blockRangeState) { for { select { - case ev := <-st.syncSub.Chan(): - if ev == nil { - continue - } - if _, ok := ev.Data.(downloader.StartEvent); ok && h.downloader.ConfigSyncMode() == ethconfig.SnapSync { + case ev := <-st.syncCh: + if ev.Type == downloader.SyncStarted && ev.Mode == ethconfig.SnapSync { h.blockRangeWhileSnapSyncing(st) } case <-st.headCh: @@ -593,12 +586,8 @@ func (h *handler) blockRangeWhileSnapSyncing(st *blockRangeState) { h.broadcastBlockRange(st) } // back to processing head block updates when sync is done - case ev := <-st.syncSub.Chan(): - if ev == nil { - continue - } - switch ev.Data.(type) { - case downloader.FailedEvent, downloader.DoneEvent: + case ev := <-st.syncCh: + if ev.Type == downloader.SyncFailed || ev.Type == downloader.SyncCompleted { return } // ignore head updates, but exit when the subscription ends diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index a45abc90eb..d056d121d9 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -424,16 +424,20 @@ func testGetBlockBodies(t *testing.T, protocol uint) { {0, []common.Hash{backend.chain.CurrentBlock().Hash()}, []bool{true}, 1}, // The chains head block should be retrievable {0, []common.Hash{{}}, []bool{false}, 0}, // A non existent block should not be returned - // Existing and non-existing blocks interleaved should not cause problems + // Existing blocks followed by a non-existing one should stop at the gap + {0, []common.Hash{ + backend.chain.GetBlockByNumber(1).Hash(), + backend.chain.GetBlockByNumber(10).Hash(), + backend.chain.GetBlockByNumber(100).Hash(), + {}, + }, []bool{true, true, true, false}, 3}, + + // A non-existing block at the start should return nothing {0, []common.Hash{ {}, backend.chain.GetBlockByNumber(1).Hash(), - {}, backend.chain.GetBlockByNumber(10).Hash(), - {}, - backend.chain.GetBlockByNumber(100).Hash(), - {}, - }, []bool{false, true, false, true, false, true, false}, 3}, + }, []bool{false, true, true}, 0}, } // Run each of the tests and verify the results against the chain for i, tt := range tests { diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 7556df9af2..3254a0abc2 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -238,10 +238,12 @@ func ServiceGetBlockBodiesQuery(chain *core.BlockChain, query GetBlockBodiesRequ lookups >= 2*maxBodiesServe { break } - if data := chain.GetBodyRLP(hash); len(data) != 0 { - bodies = append(bodies, data) - bytes += len(data) + data := chain.GetBodyRLP(hash) + if len(data) == 0 { + break // If we don't have this block's body, stop serving. } + bodies = append(bodies, data) + bytes += len(data) } return bodies } @@ -281,16 +283,16 @@ func ServiceGetReceiptsQuery69(chain *core.BlockChain, query GetReceiptsRequest) // Retrieve the requested block's receipts results := chain.GetReceiptsRLP(hash) if results == nil { - continue // Can't retrieve the receipts, so we just skip this block. + break // Don't have this block's receipts, stop serving. } body := chain.GetBodyRLP(hash) if body == nil { - continue // The block body is missing, we also have to skip. + break // The block body is missing, stop serving. } results, _, err := blockReceiptsToNetwork(results, body, receiptQueryParams{}) if err != nil { log.Error("Error in block receipts conversion", "hash", hash, "err", err) - continue + break } receipts.AppendRaw(results) bytes += len(results) @@ -312,12 +314,13 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest, break } results := chain.GetReceiptsRLP(hash) + // If we don't have this block's receipts or body, stop serving. if results == nil { - continue // Can't retrieve the receipts, so we just skip this block. + break } body := chain.GetBodyRLP(hash) if body == nil { - continue // The block body is missing, we also have to skip. + break } q := receiptQueryParams{sizeLimit: uint64(maxPacketSize - bytes)} if i == 0 { @@ -326,7 +329,7 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest, results, incomplete, err := blockReceiptsToNetwork(results, body, q) if err != nil { log.Error("Error in block receipts conversion", "hash", hash, "err", err) - continue + break } if results == nil { // This case triggers when the first receipt of the block receipts list doesn't diff --git a/eth/state_accessor.go b/eth/state_accessor.go index a806a4fc56..284ddf4305 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -248,13 +248,10 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) evm := vm.NewEVM(context, statedb, eth.blockchain.Config(), vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - // If prague hardfork, insert parent block hash in the state as per EIP-2935. - if eth.blockchain.Config().IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), eth.blockchain.Config(), evm, block.Number(), block.Time()) + if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil } @@ -268,7 +265,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) // Not yet the searched for transaction, execute on top of the current state - statedb.SetTxContext(tx.Hash(), idx) + statedb.SetTxContext(tx.Hash(), idx, uint32(idx+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } diff --git a/eth/syncer/syncer.go b/eth/syncer/syncer.go index c0d54b953b..b04d8f22e8 100644 --- a/eth/syncer/syncer.go +++ b/eth/syncer/syncer.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -37,32 +38,40 @@ type syncReq struct { errc chan error } +type Config struct { + TargetBlock common.Hash // if set, sync is triggered at startup + ExitWhenSynced bool // if true, the node shuts down after sync has finished +} + // Syncer is an auxiliary service that allows Geth to perform full sync // alone without consensus-layer attached. Users must specify a valid block hash // as the sync target. // +// Additionally, the syncer can be used to monitor state synchronization. +// It will exit once the specified target has been reached or when the +// most recent chain head is caught up. +// // This tool can be applied to different networks, no matter it's pre-merge or // post-merge, but only for full-sync. type Syncer struct { - stack *node.Node - backend *eth.Ethereum - target common.Hash - request chan *syncReq - closed chan struct{} - wg sync.WaitGroup - exitWhenSynced bool + stack *node.Node + backend *eth.Ethereum + request chan *syncReq + closed chan struct{} + wg sync.WaitGroup + + config Config } // Register registers the synchronization override service into the node // stack for launching and stopping the service controlled by node. -func Register(stack *node.Node, backend *eth.Ethereum, target common.Hash, exitWhenSynced bool) (*Syncer, error) { +func Register(stack *node.Node, backend *eth.Ethereum, cfg Config) (*Syncer, error) { s := &Syncer{ - stack: stack, - backend: backend, - target: target, - request: make(chan *syncReq), - closed: make(chan struct{}), - exitWhenSynced: exitWhenSynced, + stack: stack, + backend: backend, + request: make(chan *syncReq), + closed: make(chan struct{}), + config: cfg, } stack.RegisterAPIs(s.APIs()) stack.RegisterLifecycle(s) @@ -88,9 +97,11 @@ func (s *Syncer) run() { var ( target *types.Header - ticker = time.NewTicker(time.Second * 5) + syncCh = make(chan downloader.SyncEvent, 10) ) - defer ticker.Stop() + sub := s.backend.Downloader().SubscribeSyncEvents(syncCh) + defer sub.Unsubscribe() + for { select { case req := <-s.request: @@ -137,35 +148,50 @@ func (s *Syncer) run() { } } - case <-ticker.C: - if target == nil { + case ev := <-syncCh: + if ev.Type == downloader.SyncStarted { + log.Debug("Synchronization started") continue } + if ev.Type == downloader.SyncFailed { + log.Debug("Synchronization failed", "err", ev.Err) + continue + } + + head := s.backend.BlockChain().CurrentHeader() + if head != nil { + // Set the finalized and safe markers relative to the current head. + // The finalized marker is set two epochs behind the target, + // and the safe marker is set one epoch behind the target. + if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil { + if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 { + s.backend.BlockChain().SetFinalized(header) + } + } + if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil { + if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 { + s.backend.BlockChain().SetSafe(header) + } + } + } // Terminate the node if the target has been reached - if s.exitWhenSynced { - if block := s.backend.BlockChain().GetBlockByHash(target.Hash()); block != nil { - log.Info("Sync target reached", "number", block.NumberU64(), "hash", block.Hash()) - go s.stack.Close() // async since we need to close ourselves - return + if s.config.ExitWhenSynced { + var synced bool + var block *types.Header + if target != nil { + tb := s.backend.BlockChain().GetBlockByHash(target.Hash()) + synced = tb != nil + block = tb.Header() + } else { + timestamp := time.Unix(int64(ev.Latest.Time), 0) + synced = time.Since(timestamp) < 10*time.Minute + block = ev.Latest } - } - // Set the finalized and safe markers relative to the current head. - // The finalized marker is set two epochs behind the target, - // and the safe marker is set one epoch behind the target. - head := s.backend.BlockChain().CurrentHeader() - if head == nil { - continue - } - if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil { - if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 { - s.backend.BlockChain().SetFinalized(header) - } - } - if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil { - if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 { - s.backend.BlockChain().SetSafe(header) + if synced { + log.Info("Sync target reached", "number", block.Number.Uint64(), "hash", block.Hash()) + go s.stack.Close() // async since we need to close ourselves } } @@ -179,10 +205,10 @@ func (s *Syncer) run() { func (s *Syncer) Start() error { s.wg.Add(1) go s.run() - if s.target == (common.Hash{}) { + if s.config.TargetBlock == (common.Hash{}) { return nil } - return s.Sync(s.target) + return s.Sync(s.config.TargetBlock) } // Stop terminates the synchronization service and stop all background activities. diff --git a/eth/tracers/api.go b/eth/tracers/api.go index dae11b81de..88132b4b63 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -372,13 +372,8 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed // as per EIP-4788. context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{}) - if beaconRoot := next.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - // Insert parent hash in history contract. - if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) { - core.ProcessParentBlockHash(next.ParentHash(), evm) - } + + core.PreExecution(ctx, next.BeaconRoot(), next.ParentHash(), api.backend.ChainConfig(), evm, next.Number(), next.Time()) evm.Release() // Clean out any pending release functions of trace state. Note this // step must be done after constructing tracing state, because the @@ -494,8 +489,8 @@ func (api *API) StandardTraceBlockToFile(ctx context.Context, hash common.Hash, return api.standardTraceBlockToFile(ctx, block, config) } -// IntermediateRoots executes a block (bad- or canon- or side-), and returns a list -// of intermediate roots: the stateroot after each transaction. +// IntermediateRoots executes a block, and returns a list of intermediate roots: +// the stateroot after each transaction. func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config *TraceConfig) ([]common.Hash, error) { block, _ := api.blockByHash(ctx, hash) if block == nil { @@ -517,27 +512,25 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config return nil, err } defer release() + var ( roots []common.Hash signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) + evm = vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) ) - evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if chainConfig.IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + for i, tx := range block.Transactions() { if err := ctx.Err(); err != nil { return nil, err } msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err) // We intentionally don't return the error here: if we do, then the RPC server will not @@ -548,7 +541,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config // N.B: This should never happen while tracing canon blocks, only when tracing bad blocks. return roots, nil } - // calling IntermediateRoot will internally call Finalize on the state + // Calling IntermediateRoot will internally call Finalize on the state // so any modifications are written to the trie roots = append(roots, statedb.IntermediateRoot(deleteEmptyObjects)) } @@ -587,12 +580,9 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if api.backend.ChainConfig().IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), api.backend.ChainConfig(), evm, block.Number(), block.Time()) // JS tracers have high overhead. In this case run a parallel // process that generates states in one thread and traces txes @@ -691,7 +681,7 @@ txloop: // Generate the next state snapshot fast without tracing msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { failed = err break txloop @@ -760,15 +750,12 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block // Note: This copies the config, to not screw up the main config chainConfig, canon = overrideConfig(chainConfig, config.Overrides) } - evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if chainConfig.IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + for i, tx := range block.Transactions() { // Prepare the transaction for un-traced execution msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) @@ -795,6 +782,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block return nil, err } dumps = append(dumps, dump.Name()) + // Set up the tracer and EVM for the transaction. var ( writer = bufio.NewWriter(dump) @@ -805,7 +793,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block }) ) // Execute the transaction and flush any traces to disk - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if tracer.OnTxStart != nil { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } @@ -1028,9 +1016,9 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor defer cancel() // Call Prepare to clear out the statedb access list - statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) + statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1)) - _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) + _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go index 692c5eb775..39067e8efc 100644 --- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go +++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go @@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) { } context := core.NewEVMBlockContext(block.Header(), blockchain, nil) evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()}) - _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) + _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/live/noop.go b/eth/tracers/live/noop.go index f3def85606..b1784dbd91 100644 --- a/eth/tracers/live/noop.go +++ b/eth/tracers/live/noop.go @@ -47,6 +47,7 @@ func newNoopTracer(_ json.RawMessage) (*tracing.Hooks, error) { OnOpcode: t.OnOpcode, OnFault: t.OnFault, OnGasChange: t.OnGasChange, + OnGasChangeV2: t.OnGasChangeV2, OnBlockchainInit: t.OnBlockchainInit, OnBlockStart: t.OnBlockStart, OnBlockEnd: t.OnBlockEnd, @@ -113,3 +114,6 @@ func (t *noop) OnBlockHashRead(number uint64, hash common.Hash) {} func (t *noop) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { } + +func (t *noop) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) { +} diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go index 749aade61b..31c3ebde93 100644 --- a/eth/tracers/logger/access_list_tracer.go +++ b/eth/tracers/logger/access_list_tracer.go @@ -112,9 +112,10 @@ type AccessListTracer struct { func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer { list := newAccessList() for _, al := range acl { - if _, ok := addressesToExclude[al.Address]; !ok { - list.addAddress(al.Address) + if _, ok := addressesToExclude[al.Address]; ok { + continue } + list.addAddress(al.Address) for _, slot := range al.StorageKeys { list.addSlot(al.Address, slot) } diff --git a/eth/tracers/logger/access_list_tracer_test.go b/eth/tracers/logger/access_list_tracer_test.go new file mode 100644 index 0000000000..04b2b4b31b --- /dev/null +++ b/eth/tracers/logger/access_list_tracer_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package logger + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestNewAccessListTracerExcludedAddress(t *testing.T) { + excluded := common.HexToAddress("0x2222222222222222222222222222222222222222") + slot := common.HexToHash("0x01") + prelude := types.AccessList{{ + Address: excluded, + StorageKeys: []common.Hash{slot}, + }} + excl := map[common.Address]struct{}{excluded: {}} + tracer := NewAccessListTracer(prelude, excl) + got := tracer.AccessList() + if len(got) != 0 { + t.Fatalf("excluded prelude address must not contribute tuples, got %+v", got) + } +} diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 7f2b2aecf2..8e445818ef 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -229,9 +229,9 @@ type StructLogger struct { logs []json.RawMessage // buffer of json-encoded logs resultSize int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - skip bool // skip processing hooks. + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop + skip bool // skip processing hooks. } // NewStreamingStructLogger returns a new streaming logger. @@ -357,8 +357,8 @@ func (l *StructLogger) OnExit(depth int, output []byte, gasUsed uint64, err erro func (l *StructLogger) GetResult() (json.RawMessage, error) { // Tracing aborted - if l.reason != nil { - return nil, l.reason + if p := l.reason.Load(); p != nil { + return nil, *p } failed := l.err != nil returnData := common.CopyBytes(l.output) @@ -376,7 +376,7 @@ func (l *StructLogger) GetResult() (json.RawMessage, error) { // Stop terminates execution of the tracer at the first opportune moment. func (l *StructLogger) Stop(err error) { - l.reason = err + l.reason.Store(&err) l.interrupt.Store(true) } diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index cec45a1e7a..a542eeffa2 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -49,9 +49,9 @@ func init() { // 0xc281d19e-0: 1 // } type fourByteTracer struct { - ids map[string]int // ids aggregates the 4byte ids found - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + ids map[string]int // ids aggregates the 4byte ids found + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop chainConfig *params.ChainConfig activePrecompiles []common.Address // Updated on tx start based on given rules } @@ -124,12 +124,15 @@ func (t *fourByteTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.reason + if p := t.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *fourByteTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 06220da84d..dfa804827b 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -116,8 +116,8 @@ type callTracer struct { config callTracerConfig gasLimit uint64 depth int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop } type callTracerConfig struct { @@ -268,12 +268,15 @@ func (t *callTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.reason + if p := t.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *callTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/call_flat.go b/eth/tracers/native/call_flat.go index 4e7fc31a9c..484f2d4e3b 100644 --- a/eth/tracers/native/call_flat.go +++ b/eth/tracers/native/call_flat.go @@ -233,7 +233,10 @@ func (t *flatCallTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.tracer.reason + if p := t.tracer.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 34e202f667..0bf80d77b5 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -135,8 +135,8 @@ type opcodeWithPartialStack struct { type erc7562Tracer struct { config erc7562TracerConfig gasLimit uint64 - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop env *tracing.VMContext ignoredOpcodes map[vm.OpCode]struct{} @@ -317,7 +317,10 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { // error arising from the encoding or forceful termination (via `Stop`). func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { if t.interrupt.Load() { - return nil, t.reason + if p := t.reason.Load(); p != nil { + return nil, *p + } + return nil, nil } if len(t.callstackWithOpcodes) != 1 { return nil, errors.New("incorrect number of top-level calls") @@ -337,12 +340,15 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { return nil, err } - return enc, t.reason + if p := t.reason.Load(); p != nil { + return enc, *p + } + return enc, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *erc7562Tracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index b7d6f29a6a..73f8585a6b 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -63,22 +63,31 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf // // The names parameter associates a label with each tracer, used as keys in // the aggregated JSON result returned by GetResult. +// +// For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2, +// OnNonceChange / OnNonceChangeV2, OnGasChange / OnGasChangeV2, +// OnSystemCallStart / OnSystemCallStartV2), the mux exposes only the V2 +// variant upward. The fanout then prefers each child's V2 hook and falls +// back to V1 if only V1 is set, mirroring the precedence already used in +// core/state_processor.go. func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) { t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ Hooks: &tracing.Hooks{ - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnEnter: t.OnEnter, - OnExit: t.OnExit, - OnOpcode: t.OnOpcode, - OnFault: t.OnFault, - OnGasChange: t.OnGasChange, - OnBalanceChange: t.OnBalanceChange, - OnNonceChange: t.OnNonceChange, - OnCodeChange: t.OnCodeChange, - OnStorageChange: t.OnStorageChange, - OnLog: t.OnLog, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnEnter: t.OnEnter, + OnExit: t.OnExit, + OnOpcode: t.OnOpcode, + OnFault: t.OnFault, + OnGasChangeV2: t.OnGasChangeV2, + OnBalanceChange: t.OnBalanceChange, + OnNonceChangeV2: t.OnNonceChangeV2, + OnCodeChangeV2: t.OnCodeChangeV2, + OnStorageChange: t.OnStorageChange, + OnLog: t.OnLog, + OnSystemCallStartV2: t.OnSystemCallStart, + OnSystemCallEnd: t.OnSystemCallEnd, }, GetResult: t.GetResult, Stop: t.Stop, @@ -101,10 +110,12 @@ func (t *muxTracer) OnFault(pc uint64, op byte, gas, cost uint64, scope tracing. } } -func (t *muxTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { +func (t *muxTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) { for _, t := range t.tracers { - if t.OnGasChange != nil { - t.OnGasChange(old, new, reason) + if t.OnGasChangeV2 != nil { + t.OnGasChangeV2(old, new, reason) + } else if t.OnGasChange != nil { + t.OnGasChange(old.Regular, new.Regular, reason) } } } @@ -149,26 +160,22 @@ func (t *muxTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason } } -func (t *muxTracer) OnNonceChange(a common.Address, prev, new uint64) { +func (t *muxTracer) OnNonceChangeV2(a common.Address, prev, new uint64, reason tracing.NonceChangeReason) { for _, t := range t.tracers { - if t.OnNonceChange != nil { + if t.OnNonceChangeV2 != nil { + t.OnNonceChangeV2(a, prev, new, reason) + } else if t.OnNonceChange != nil { t.OnNonceChange(a, prev, new) } } } -func (t *muxTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { - for _, t := range t.tracers { - if t.OnCodeChange != nil { - t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) - } - } -} - func (t *muxTracer) OnCodeChangeV2(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { for _, t := range t.tracers { if t.OnCodeChangeV2 != nil { t.OnCodeChangeV2(a, prevCodeHash, prev, codeHash, code, reason) + } else if t.OnCodeChange != nil { + t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) } } } @@ -189,6 +196,24 @@ func (t *muxTracer) OnLog(log *types.Log) { } } +func (t *muxTracer) OnSystemCallStart(vm *tracing.VMContext) { + for _, t := range t.tracers { + if t.OnSystemCallStartV2 != nil { + t.OnSystemCallStartV2(vm) + } else if t.OnSystemCallStart != nil { + t.OnSystemCallStart() + } + } +} + +func (t *muxTracer) OnSystemCallEnd() { + for _, t := range t.tracers { + if t.OnSystemCallEnd != nil { + t.OnSystemCallEnd() + } + } +} + // GetResult returns an empty json object. func (t *muxTracer) GetResult() (json.RawMessage, error) { resObject := make(map[string]json.RawMessage) diff --git a/eth/tracers/native/mux_test.go b/eth/tracers/native/mux_test.go new file mode 100644 index 0000000000..902b7a026a --- /dev/null +++ b/eth/tracers/native/mux_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +// TestMuxForwardsV2StateHooks verifies that the mux tracer fans out the V2 +// variants of state-change hooks to child tracers. A child tracer that only +// implements OnCodeChangeV2 / OnNonceChangeV2 must still receive events when +// wrapped behind the mux. The mux must also fall back to the V1 hook when a +// child only implements V1, mirroring the precedence used in +// core/state_processor.go. +func TestMuxForwardsV2StateHooks(t *testing.T) { + var ( + codeV2Calls int + nonceV2Calls int + codeV1Calls int + nonceV1Calls int + ) + v2Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChangeV2: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { + codeV2Calls++ + }, + OnNonceChangeV2: func(addr common.Address, prev, new uint64, reason tracing.NonceChangeReason) { + nonceV2Calls++ + }, + }, + } + v1Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChange: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) { + codeV1Calls++ + }, + OnNonceChange: func(addr common.Address, prev, new uint64) { + nonceV1Calls++ + }, + }, + } + mux, err := NewMuxTracer([]string{"v2", "v1"}, []*tracers.Tracer{v2Child, v1Child}) + if err != nil { + t.Fatalf("NewMuxTracer: %v", err) + } + + if mux.Hooks.OnCodeChangeV2 == nil { + t.Fatal("mux does not expose OnCodeChangeV2; V2-only child tracers will miss code changes") + } + if mux.Hooks.OnNonceChangeV2 == nil { + t.Fatal("mux does not expose OnNonceChangeV2; V2-only child tracers will miss nonce changes") + } + + mux.Hooks.OnCodeChangeV2(common.Address{}, common.Hash{}, nil, common.Hash{}, nil, tracing.CodeChangeContractCreation) + mux.Hooks.OnNonceChangeV2(common.Address{}, 0, 1, tracing.NonceChangeEoACall) + + if codeV2Calls != 1 { + t.Fatalf("V2 child OnCodeChangeV2 got %d calls, want 1", codeV2Calls) + } + if nonceV2Calls != 1 { + t.Fatalf("V2 child OnNonceChangeV2 got %d calls, want 1", nonceV2Calls) + } + if codeV1Calls != 1 { + t.Fatalf("V1 child OnCodeChange got %d calls, want 1 (mux should fall back from V2 to V1)", codeV1Calls) + } + if nonceV1Calls != 1 { + t.Fatalf("V1 child OnNonceChange got %d calls, want 1 (mux should fall back from V2 to V1)", nonceV1Calls) + } +} diff --git a/eth/tracers/native/noop.go b/eth/tracers/native/noop.go index ac174cc25e..323bf4338f 100644 --- a/eth/tracers/native/noop.go +++ b/eth/tracers/native/noop.go @@ -47,6 +47,7 @@ func newNoopTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *param OnOpcode: t.OnOpcode, OnFault: t.OnFault, OnGasChange: t.OnGasChange, + OnGasChangeV2: t.OnGasChangeV2, OnBalanceChange: t.OnBalanceChange, OnNonceChange: t.OnNonceChange, OnCodeChange: t.OnCodeChange, @@ -66,6 +67,8 @@ func (t *noopTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpC func (t *noopTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {} +func (t *noopTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {} + func (t *noopTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { } diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index 36cb16e44b..7026cca7f3 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -71,8 +71,8 @@ type prestateTracer struct { to common.Address config PrestateTracerConfig chainConfig *params.ChainConfig - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop created map[common.Address]bool deleted map[common.Address]bool } @@ -240,12 +240,15 @@ func (t *prestateTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return json.RawMessage(res), t.reason + if p := t.reason.Load(); p != nil { + return json.RawMessage(res), *p + } + return json.RawMessage(res), nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *prestateTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/tracer_test.go b/eth/tracers/native/tracer_test.go new file mode 100644 index 0000000000..70e6283d34 --- /dev/null +++ b/eth/tracers/native/tracer_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native_test + +import ( + "errors" + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestTracerStopRace exercises the concurrent Stop / GetResult path that the +// trace RPC handler uses: a timeout watchdog goroutine calls Stop while the +// main goroutine is still running the trace and will eventually call +// GetResult. Under -race, writes to the interruption reason field must not +// race with reads, for every tracer that implements it. +// +// callTracer, flatCallTracer and erc7562Tracer's GetResult short-circuits on +// an empty callstack ("incorrect number of top-level calls") before loading +// the reason. For those tracers the test pushes a single top-level call frame +// via OnEnter so GetResult reaches the reason.Load() path where the race can +// be observed under -race. +func TestTracerStopRace(t *testing.T) { + type setup struct { + name string + needsFrame bool // whether GetResult requires a top-level call frame + } + cases := []setup{ + {"callTracer", true}, + {"flatCallTracer", true}, + {"4byteTracer", false}, + {"prestateTracer", false}, + {"erc7562Tracer", true}, + } + for _, s := range cases { + t.Run(s.name, func(t *testing.T) { + tr, err := tracers.DefaultDirectory.New(s.name, &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + if s.needsFrame && tr.OnEnter != nil { + // Push a single top-level call frame so GetResult doesn't + // short-circuit before reading the interruption reason. + tr.OnEnter(0, byte(vm.CALL), common.Address{}, common.Address{}, nil, 0, big.NewInt(0)) + } + + stopErr := errors.New("execution timeout") + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + tr.Stop(stopErr) + }() + go func() { + defer wg.Done() + _, _ = tr.GetResult() + }() + wg.Wait() + }) + } +} diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 412f8955ba..1d8573f982 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -914,6 +914,7 @@ type SimulateCallResult struct { ReturnValue []byte `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed uint64 `json:"gasUsed"` + MaxUsedGas uint64 `json:"maxUsedGas"` Status uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -921,6 +922,7 @@ type SimulateCallResult struct { type simulateCallResultMarshaling struct { ReturnValue hexutil.Bytes GasUsed hexutil.Uint64 + MaxUsedGas hexutil.Uint64 Status hexutil.Uint64 } diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index f9e761e412..fb04d77669 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -861,6 +861,12 @@ func TestSimulateV1(t *testing.T) { if results[0].Calls[0].Error != nil { t.Errorf("expected no error, got %v", results[0].Calls[0].Error) } + if results[0].Calls[0].MaxUsedGas == 0 { + t.Error("expected maxUsedGas to be set") + } + if results[0].Calls[0].MaxUsedGas < results[0].Calls[0].GasUsed { + t.Errorf("expected maxUsedGas >= gasUsed, got %d < %d", results[0].Calls[0].MaxUsedGas, results[0].Calls[0].GasUsed) + } } func TestSimulateV1WithBlockOverrides(t *testing.T) { diff --git a/ethclient/gen_simulate_call_result.go b/ethclient/gen_simulate_call_result.go index 55e14cd697..18373bbb88 100644 --- a/ethclient/gen_simulate_call_result.go +++ b/ethclient/gen_simulate_call_result.go @@ -17,6 +17,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) { ReturnValue hexutil.Bytes `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed hexutil.Uint64 `json:"gasUsed"` + MaxUsedGas hexutil.Uint64 `json:"maxUsedGas"` Status hexutil.Uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -24,6 +25,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) { enc.ReturnValue = s.ReturnValue enc.Logs = s.Logs enc.GasUsed = hexutil.Uint64(s.GasUsed) + enc.MaxUsedGas = hexutil.Uint64(s.MaxUsedGas) enc.Status = hexutil.Uint64(s.Status) enc.Error = s.Error return json.Marshal(&enc) @@ -35,6 +37,7 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error { ReturnValue *hexutil.Bytes `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed *hexutil.Uint64 `json:"gasUsed"` + MaxUsedGas *hexutil.Uint64 `json:"maxUsedGas"` Status *hexutil.Uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -51,6 +54,9 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error { if dec.GasUsed != nil { s.GasUsed = uint64(*dec.GasUsed) } + if dec.MaxUsedGas != nil { + s.MaxUsedGas = uint64(*dec.MaxUsedGas) + } if dec.Status != nil { s.Status = uint64(*dec.Status) } diff --git a/ethclient/simulated/backend.go b/ethclient/simulated/backend.go index d573c7e750..160ad924bf 100644 --- a/ethclient/simulated/backend.go +++ b/ethclient/simulated/backend.go @@ -86,6 +86,8 @@ func NewBackend(alloc types.GenesisAlloc, options ...func(nodeConf *node.Config, } ethConf.SyncMode = ethconfig.FullSync ethConf.TxPool.NoLocals = true + // Disable log indexing to force unindexed log search + ethConf.LogNoHistory = true for _, option := range options { option(&nodeConf, ðConf) diff --git a/internal/build/gotool.go b/internal/build/gotool.go index 172fa13464..00aa9d6f02 100644 --- a/internal/build/gotool.go +++ b/internal/build/gotool.go @@ -41,12 +41,19 @@ type GoToolchain struct { func (g *GoToolchain) Go(command string, args ...string) *exec.Cmd { tool := g.goTool(command, args...) - // Configure environment for cross build. - if g.GOARCH != "" && g.GOARCH != runtime.GOARCH { + // Configure environment for cross build. Force CGO_ENABLED=1 whenever + // either GOOS or GOARCH differs from the host: Go's default is + // CGO_ENABLED=0 for any cross-compile, but geth's release builds rely + // on cgo (c-kzg-4844, secp256k1) regardless of which axis is crossing. + crossArch := g.GOARCH != "" && g.GOARCH != runtime.GOARCH + crossOS := g.GOOS != "" && g.GOOS != runtime.GOOS + if crossArch || crossOS { tool.Env = append(tool.Env, "CGO_ENABLED=1") + } + if crossArch { tool.Env = append(tool.Env, "GOARCH="+g.GOARCH) } - if g.GOOS != "" && g.GOOS != runtime.GOOS { + if crossOS { tool.Env = append(tool.Env, "GOOS="+g.GOOS) } // Configure C compiler. diff --git a/internal/download/download.go b/internal/download/download.go index c59c8a90c3..94517166f5 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -22,6 +22,7 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "errors" "fmt" "io" "iter" @@ -180,12 +181,13 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error { return fmt.Errorf("no known hash for file %q", basename) } // Shortcut if already downloaded. - if verifyHash(dstPath, hash) == nil { + if err := verifyHash(dstPath, hash); err == nil { fmt.Printf("%s is up-to-date\n", dstPath) return nil + } else if !errors.Is(err, os.ErrNotExist) { + fmt.Printf("%s is stale\n", dstPath) } - fmt.Printf("%s is stale\n", dstPath) fmt.Printf("downloading from %s\n", url) resp, err := http.Get(url) if err != nil { @@ -209,9 +211,12 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error { if resp.ContentLength > 0 { dst = newDownloadWriter(fd, resp.ContentLength) } - _, err = io.Copy(dst, resp.Body) - dst.Close() - if err != nil { + if _, err = io.Copy(dst, resp.Body); err != nil { + dst.Close() + os.Remove(tmpfile) + return err + } + if err = dst.Close(); err != nil { os.Remove(tmpfile) return err } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index e8669b86c6..109169e0b0 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -146,6 +146,11 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big { return (*hexutil.Big)(api.b.BlobBaseFee(ctx)) } +// BaseFee returns the base fee for the next block. +func (api *EthereumAPI) BaseFee(ctx context.Context) *hexutil.Big { + return (*hexutil.Big)(api.b.BaseFee(ctx)) +} + // Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not // yet received the latest block headers from its peers. In case it is synchronizing: // - startingBlock: block number this node started to synchronize from @@ -734,6 +739,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S if err := blockOverrides.Apply(&blockCtx); err != nil { return nil, err } + // Override the header so callers that compute gas price from 1559 fee + // fields see the overridden basefee. Otherwise GASPRICE/effectiveTip + // would be derived from the pre-override basefee. + header = blockOverrides.MakeHeader(header) } rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time) precompiles := vm.ActivePrecompiledContracts(rules) @@ -992,6 +1001,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.RequestsHash != nil { result["requestsHash"] = head.RequestsHash } + if head.BlockAccessListHash != nil { + result["balHash"] = head.BlockAccessListHash + } if head.SlotNumber != nil { result["slotNumber"] = hexutil.Uint64(*head.SlotNumber) } @@ -2119,8 +2131,7 @@ func (api *DebugAPI) SetHead(number hexutil.Uint64) error { if header.Number.Uint64() <= uint64(number) { return errors.New("not allowed to rewind to a future block") } - api.b.SetHead(uint64(number)) - return nil + return api.b.SetHead(uint64(number)) } // NetAPI offers network related RPC methods diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 6cf52d636a..561ce2c2d2 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -500,6 +500,7 @@ func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBloc return nil, nil, nil, nil, nil, nil, nil } func (b testBackend) BlobBaseFee(ctx context.Context) *big.Int { return new(big.Int) } +func (b testBackend) BaseFee(ctx context.Context) *big.Int { return new(big.Int) } func (b testBackend) ChainDb() ethdb.Database { return b.db } func (b testBackend) AccountManager() *accounts.Manager { return b.accman } func (b testBackend) ExtRPCEnabled() bool { return false } @@ -507,7 +508,7 @@ func (b testBackend) RPCGasCap() uint64 { return 10000000 func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second } func (b testBackend) RPCTxFeeCap() float64 { return 0 } func (b testBackend) UnprotectedAllowed() bool { return false } -func (b testBackend) SetHead(number uint64) {} +func (b testBackend) SetHead(number uint64) error { return nil } func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { if number == rpc.LatestBlockNumber { return b.chain.CurrentBlock(), nil @@ -1315,6 +1316,27 @@ func TestCall(t *testing.T) { }, expectErr: errors.New(`block override "withdrawals" is not supported for this RPC method`), }, + // Verify that an overridden basefee is honored when computing gasPrice + // from the 1559 fee fields. Returning GASPRICE opcode; expected value + // is min(MaxFeePerGas, MaxPriorityFeePerGas + overridden BaseFee). + // + // BaseFee override = 0xa (10); MaxFeePerGas = 0x64 (100); + // MaxPriorityFeePerGas = 0x2 (2); expected GASPRICE = 12. + { + name: "basefee-override-used-in-gasprice", + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + // Contract: GASPRICE; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN + Input: hex2Bytes("3a60005260206000f3"), + MaxFeePerGas: (*hexutil.Big)(big.NewInt(100)), + MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(2)), + }, + blockOverrides: override.BlockOverrides{ + BaseFeePerGas: (*hexutil.Big)(big.NewInt(10)), + }, + want: "0x000000000000000000000000000000000000000000000000000000000000000c", + }, } for _, tc := range testSuite { result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides) @@ -2659,6 +2681,67 @@ func TestSimulateV1TxSender(t *testing.T) { require.Equal(t, sender2, summary[1].Transactions[0].From, "sender address mismatch") } +// TestSimulateV1WithdrawalsByFork verifies that withdrawals and withdrawalsRoot +// are only emitted in the simulated block result when the simulated block is +// post-Shanghai. Pre-Shanghai blocks must omit both fields, otherwise the +// header hash and size would not match a valid pre-Shanghai block. +func TestSimulateV1WithdrawalsByFork(t *testing.T) { + t.Parallel() + + run := func(t *testing.T, cfg *params.ChainConfig, blockTime *uint64, wantWithdrawals bool) { + t.Helper() + gspec := &core.Genesis{Config: cfg, Alloc: types.GenesisAlloc{}} + backend := newTestBackend(t, 1, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {}) + + ctx := context.Background() + stateDB, baseHeader, err := backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) + if err != nil { + t.Fatalf("failed to get state and header: %v", err) + } + sim := &simulator{ + b: backend, + state: stateDB, + base: baseHeader, + chainConfig: backend.ChainConfig(), + budget: newGasBudget(0), + } + + block := simBlock{} + if blockTime != nil { + t := hexutil.Uint64(*blockTime) + block.BlockOverrides = &override.BlockOverrides{Time: &t} + } + results, err := sim.execute(ctx, []simBlock{block}) + if err != nil { + t.Fatalf("simulation execution failed: %v", err) + } + require.Len(t, results, 1) + + enc, err := json.Marshal(results[0]) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(enc, &raw); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + _, hasWithdrawals := raw["withdrawals"] + _, hasWithdrawalsRoot := raw["withdrawalsRoot"] + if hasWithdrawals != wantWithdrawals || hasWithdrawalsRoot != wantWithdrawals { + t.Fatalf("unexpected withdrawals fields: withdrawals=%v withdrawalsRoot=%v want=%v\n%s", hasWithdrawals, hasWithdrawalsRoot, wantWithdrawals, enc) + } + } + + t.Run("pre-shanghai", func(t *testing.T) { + // TestChainConfig has ShanghaiTime=nil, so all simulated blocks are pre-Shanghai. + run(t, params.TestChainConfig, nil, false) + }) + t.Run("post-shanghai", func(t *testing.T) { + // MergedTestChainConfig has every fork active from genesis. + run(t, params.MergedTestChainConfig, nil, true) + }) +} + func TestSignTransaction(t *testing.T) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index af3d592b82..f23be85782 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -46,6 +46,7 @@ type Backend interface { SuggestGasTipCap(ctx context.Context) (*big.Int, error) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) BlobBaseFee(ctx context.Context) *big.Int + BaseFee(ctx context.Context) *big.Int ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool @@ -57,7 +58,7 @@ type Backend interface { RPCTxSyncMaxTimeout() time.Duration // Blockchain API - SetHead(number uint64) + SetHead(number uint64) error HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index e3a14bf5d6..8462194b1d 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/ethapi/override" "github.com/ethereum/go-ethereum/params" @@ -292,9 +293,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, gp = core.NewGasPool(blockContext.GasLimit) blobGasUsed uint64 - txes = make([]*types.Transaction, len(block.Calls)) - callResults = make([]simCallResult, len(block.Calls)) - receipts = make([]*types.Receipt, len(block.Calls)) + txes = make([]*types.Transaction, len(block.Calls)) + callResults = make([]simCallResult, len(block.Calls)) + receipts = make([]*types.Receipt, len(block.Calls)) + blockAccessList = bal.NewConstructionBlockAccessList() // Block hash will be repaired after execution. tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0) @@ -313,17 +315,15 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig) defer evm.Release() + // It is possible to override precompiles with EVM bytecode, or // move them to another address. if precompiles != nil { evm.SetPrecompiles(precompiles) } - if sim.chainConfig.IsPrague(header.Number, header.Time) || sim.chainConfig.IsUBT(header.Number, header.Time) { - core.ProcessParentBlockHash(header.ParentHash, evm) - } - if header.ParentBeaconRoot != nil { - core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, evm) - } + // Run pre-execution system calls + blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)) + var allLogs []*types.Log for i, call := range block.Calls { // Terminate if the context is cancelled @@ -343,7 +343,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, tracer.reset(txHash, uint(i)) // EoA check is always skipped, even in validation mode. - sim.state.SetTxContext(txHash, i) + sim.state.SetTxContext(txHash, i, uint32(i+1)) msg := call.ToMessage(header.BaseFee, !sim.validate) result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp) if err != nil { @@ -353,7 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, // Update the state with pending changes. var root []byte if sim.chainConfig.IsByzantium(blockContext.BlockNumber) { - tracingStateDB.Finalise(true) + blockAccessList.Merge(tracingStateDB.Finalise(true)) } else { root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes() } @@ -394,35 +394,32 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } // Process EIP-7685 requests - var requests [][]byte - if sim.chainConfig.IsPrague(header.Number, header.Time) { - requests = [][]byte{} - // EIP-6110 - if err := core.ParseDepositLogs(&requests, allLogs, sim.chainConfig); err != nil { - return nil, nil, nil, err - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { - return nil, nil, nil, err - } - // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { - return nil, nil, nil, err - } + requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) + if err != nil { + return nil, nil, nil, err } if requests != nil { reqHash := types.CalcRequestsHash(requests) header.RequestsHash = &reqHash } + blockAccessList.Merge(bal) blockBody := &types.Body{ Transactions: txes, - Withdrawals: *block.BlockOverrides.Withdrawals, // Withdrawal is also sanitized as non-nil + } + // Withdrawals are a post-Shanghai field. Attaching a non-nil withdrawals + // slice would cause types.NewBlock to populate WithdrawalsHash on the + // header and emit withdrawals fields for pre-Shanghai blocks. + if sim.chainConfig.IsShanghai(header.Number, header.Time) { + blockBody.Withdrawals = *block.BlockOverrides.Withdrawals } chainHeadReader := &simChainHeadReader{ctx, sim.b} + // Apply the consensus-specific post-transaction changes + sim.b.Engine().Finalize(chainHeadReader, header, sim.state, blockBody, uint32(len(block.Calls)+1), blockAccessList) + // Assemble the block - b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts) + b := core.AssembleBlock(chainHeadReader, header, sim.state, blockBody, receipts, blockAccessList) repairLogs(callResults, b.Hash()) return b, callResults, senders, nil diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 4fb30e6289..1032d067f1 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -446,27 +446,27 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *core.Message { var ( - gasPrice *big.Int - gasFeeCap *big.Int - gasTipCap *big.Int + gasPrice *uint256.Int + gasFeeCap *uint256.Int + gasTipCap *uint256.Int ) if baseFee == nil { - gasPrice = args.GasPrice.ToInt() + gasPrice, _ = args.GasPrice.ToUint256() gasFeeCap, gasTipCap = gasPrice, gasPrice } else { // A basefee is provided, necessitating 1559-type execution if args.GasPrice != nil { // User specified the legacy gas field, convert to 1559 gas typing - gasPrice = args.GasPrice.ToInt() + gasPrice, _ = args.GasPrice.ToUint256() gasFeeCap, gasTipCap = gasPrice, gasPrice } else { // User specified 1559 gas fields (or none), use those - gasFeeCap = args.MaxFeePerGas.ToInt() - gasTipCap = args.MaxPriorityFeePerGas.ToInt() + gasFeeCap, _ = args.MaxFeePerGas.ToUint256() + gasTipCap, _ = args.MaxPriorityFeePerGas.ToUint256() // Backfill the legacy gasPrice for EVM execution, unless we're all zeroes - gasPrice = new(big.Int) + gasPrice = uint256.NewInt(0) if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 { - gasPrice = gasPrice.Add(gasTipCap, baseFee) + gasPrice = gasPrice.Add(gasTipCap, uint256.MustFromBig(baseFee)) if gasPrice.Cmp(gasFeeCap) > 0 { gasPrice = gasFeeCap } @@ -477,10 +477,12 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c if args.AccessList != nil { accessList = *args.AccessList } + value, _ := args.Value.ToUint256() + blobFeeCap, _ := args.BlobFeeCap.ToUint256() return &core.Message{ From: args.from(), To: args.To, - Value: (*big.Int)(args.Value), + Value: value, Nonce: uint64(*args.Nonce), GasLimit: uint64(*args.Gas), GasPrice: gasPrice, @@ -488,7 +490,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c GasTipCap: gasTipCap, Data: args.data(), AccessList: accessList, - BlobGasFeeCap: (*big.Int)(args.BlobFeeCap), + BlobGasFeeCap: blobFeeCap, BlobHashes: args.BlobHashes, SetCodeAuthorizations: args.AuthorizationList, SkipNonceChecks: skipNonceCheck, diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 30791f32b5..ccb46a810d 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -318,6 +318,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { return big.NewInt(42), nil } func (b *backendMock) BlobBaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } +func (b *backendMock) BaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } func (b *backendMock) CurrentHeader() *types.Header { return b.current } func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config } @@ -336,7 +337,7 @@ func (b *backendMock) RPCGasCap() uint64 { return 0 } func (b *backendMock) RPCEVMTimeout() time.Duration { return time.Second } func (b *backendMock) RPCTxFeeCap() float64 { return 0 } func (b *backendMock) UnprotectedAllowed() bool { return false } -func (b *backendMock) SetHead(number uint64) {} +func (b *backendMock) SetHead(number uint64) error { return nil } func (b *backendMock) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { return nil, nil } diff --git a/miner/worker.go b/miner/worker.go index 42e3695025..21bc95cf92 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/log" @@ -71,6 +72,7 @@ type environment struct { receipts []*types.Receipt sidecars []*types.BlobTxSidecar blobs int + bal *bal.ConstructionBlockAccessList witness *stateless.Witness } @@ -167,7 +169,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, // otherwise, fill the block with the current transactions from the txpool if genParam.forceOverrides && len(genParam.overrideTxs) > 0 { for _, tx := range genParam.overrideTxs { - work.state.SetTxContext(tx.Hash(), work.tcount) + work.state.SetTxContext(tx.Hash(), work.tcount, uint32(work.tcount+1)) if err := miner.commitTransaction(ctx, work, tx); err != nil { // all passed transactions HAVE to be valid at this point return &newPayloadResult{err: err} @@ -208,29 +210,22 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } // Collect consensus-layer requests if Prague is enabled. - var requests [][]byte - if miner.chainConfig.IsPrague(work.header.Number, work.header.Time) { - requests = [][]byte{} - // EIP-6110 deposits - if err := core.ParseDepositLogs(&requests, allLogs, miner.chainConfig); err != nil { - return &newPayloadResult{err: err} - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil { - return &newPayloadResult{err: err} - } - // EIP-7251 consolidations - if err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil { - return &newPayloadResult{err: err} - } + requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) + if err != nil { + return &newPayloadResult{err: err} } if requests != nil { reqHash := types.CalcRequestsHash(requests) work.header.RequestsHash = &reqHash } + work.bal.Merge(bal) + + // Apply the consensus-specific post-transaction changes + miner.engine.Finalize(miner.chain, work.header, work.state, &body, uint32(work.tcount+1), work.bal) + // Assemble the block for delivery. _, _, assembleSpanEnd := telemetry.StartSpan(ctx, "miner.AssembleBlock") - block := core.AssembleBlock(miner.engine, miner.chain, work.header, work.state, &body, work.receipts) + block := core.AssembleBlock(miner.chain, work.header, work.state, &body, work.receipts, work.bal) assembleSpanEnd(nil) return &newPayloadResult{ @@ -329,12 +324,8 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, log.Error("Failed to create sealing context", "err", err) return nil, err } - if header.ParentBeaconRoot != nil { - core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm) - } - if miner.chainConfig.IsPrague(header.Number, header.Time) { - core.ProcessParentBlockHash(header.ParentHash, env.evm) - } + // Run pre-execution system calls + env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)) return env, nil } @@ -353,6 +344,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase } } state.StartPrefetcher("miner", bundle) + // Note the passed coinbase may be different with header.Coinbase. return &environment{ signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), @@ -361,6 +353,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, gasPool: core.NewGasPool(header.GasLimit), header: header, + bal: bal.NewConstructionBlockAccessList(), witness: state.Witness(), evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), }, nil @@ -372,7 +365,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx if tx.Type() == types.BlobTxType { return miner.commitBlobTransaction(env, tx) } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -380,6 +373,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx env.receipts = append(env.receipts, receipt) env.size += tx.Size() env.tcount++ + env.bal.Merge(bal) return nil } @@ -396,7 +390,7 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio if env.blobs+len(sc.Blobs) > maxBlobs { return errors.New("max data blobs reached") } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -408,23 +402,24 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio env.size += txNoBlob.Size() *env.header.BlobGasUsed += receipt.BlobGasUsed env.tcount++ + env.bal.Merge(bal) return nil } // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. -func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { +func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { var ( snap = env.state.Snapshot() gp = env.gasPool.Snapshot() ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) + receipt, bal, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.Set(gp) - return nil, err + return nil, nil, err } env.header.GasUsed = env.gasPool.Used() - return receipt, nil + return receipt, bal, nil } func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { @@ -518,7 +513,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl continue } // Start executing the transaction - env.state.SetTxContext(tx.Hash(), env.tcount) + env.state.SetTxContext(tx.Hash(), env.tcount, uint32(env.tcount+1)) err := miner.commitTransaction(ctx, env, tx) switch { @@ -617,10 +612,14 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 // totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order. func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int { + baseFee := block.BaseFee() feesWei := new(big.Int) + var gasUsed, product big.Int for i, tx := range block.Transactions() { - minerFee, _ := tx.EffectiveGasTip(block.BaseFee()) - feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee)) + minerFee, _ := tx.EffectiveGasTip(baseFee) + gasUsed.SetUint64(receipts[i].GasUsed) + product.Mul(&gasUsed, minerFee) + feesWei.Add(feesWei, &product) } return feesWei } diff --git a/node/defaults.go b/node/defaults.go index 403a7f88a3..3410fa2ae5 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -76,6 +76,9 @@ var DefaultConfig = Config{ DiscoveryV5: true, }, DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported + OpenTelemetry: OpenTelemetryConfig{ + SampleRatio: 1.0, + }, } // DefaultDataDir is the default data directory to use for the databases and other diff --git a/node/node.go b/node/node.go index 01318881d4..56ecd7d522 100644 --- a/node/node.go +++ b/node/node.go @@ -35,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" @@ -44,7 +43,6 @@ import ( // Node is a container on which services can be registered. type Node struct { - eventmux *event.TypeMux config *Config accman *accounts.Manager log log.Logger @@ -108,7 +106,6 @@ func New(conf *Config) (*Node, error) { node := &Node{ config: conf, inprocHandler: server, - eventmux: new(event.TypeMux), log: conf.Logger, stop: make(chan struct{}), server: &p2p.Server{Config: conf.P2P}, @@ -692,12 +689,6 @@ func (n *Node) WSAuthEndpoint() string { return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix } -// EventMux retrieves the event multiplexer used by all the network services in -// the current protocol stack. -func (n *Node) EventMux() *event.TypeMux { - return n.eventmux -} - // OpenDatabaseWithOptions opens an existing database with the given name (or creates one if no // previous can be found) from within the node's instance directory. If the node has no // data directory, an in-memory database is returned. diff --git a/node/rpcstack.go b/node/rpcstack.go index 20d488b734..1db2ed3f44 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -463,6 +463,7 @@ func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Either invalid (too many colons) or no port specified host = r.Host } + host = strings.ToLower(host) if ipAddr := net.ParseIP(host); ipAddr != nil { // It's an IP address, we can serve that h.next.ServeHTTP(w, r) diff --git a/node/rpcstack_test.go b/node/rpcstack_test.go index bd75dac4eb..f5668abb08 100644 --- a/node/rpcstack_test.go +++ b/node/rpcstack_test.go @@ -60,6 +60,9 @@ func TestVhosts(t *testing.T) { resp := rpcRequest(t, url, testMethod, "host", "test") assert.Equal(t, resp.StatusCode, http.StatusOK) + respUpper := rpcRequest(t, url, testMethod, "host", "TeSt:1234") + assert.Equal(t, respUpper.StatusCode, http.StatusOK) + resp2 := rpcRequest(t, url, testMethod, "host", "bad") assert.Equal(t, resp2.StatusCode, http.StatusForbidden) } diff --git a/p2p/dial.go b/p2p/dial.go index f9463d6d89..0ffcd10497 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -67,7 +67,10 @@ type tcpDialer struct { } func (t tcpDialer) Dial(ctx context.Context, dest *enode.Node) (net.Conn, error) { - addr, _ := dest.TCPEndpoint() + addr, ok := dest.TCPEndpoint() + if !ok { + return nil, errNoPort + } return t.d.DialContext(ctx, "tcp", addr.String()) } diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 721cd7b589..016a2d1af3 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -753,6 +753,41 @@ func (tab *Table) deleteNode(n *enode.Node) { // waitForNodes blocks until the table contains at least n nodes. func (tab *Table) waitForNodes(ctx context.Context, n int) error { + // Wrap ctx so the forwarder goroutine exits when waitForNodes returns, + // regardless of whether the caller's ctx is canceled. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Set up a notification channel that gets unblocked when there was any activity on + // the table. Ultimately this reads from the table's nodeFeed, but can't use the feed + // directly on the same goroutine that takes Table.mutex, it would deadlock. + var notify chan struct{} + var notifyErr error + initsub := func() event.Subscription { + notify = make(chan struct{}, 1) + newnode := make(chan *enode.Node, 1) + sub := tab.nodeFeed.Subscribe(newnode) + go func() { + defer close(notify) + for { + select { + case <-newnode: + select { + case notify <- struct{}{}: + default: + } + case <-ctx.Done(): + notifyErr = ctx.Err() + return + case <-tab.closeReq: + notifyErr = errClosed + return + } + } + }() + return sub + } + getlength := func() (count int) { for _, b := range &tab.buckets { count += len(b.entries) @@ -760,28 +795,24 @@ func (tab *Table) waitForNodes(ctx context.Context, n int) error { return count } - var ch chan *enode.Node for { tab.mutex.Lock() if getlength() >= n { tab.mutex.Unlock() return nil } - if ch == nil { - // Init subscription. - ch = make(chan *enode.Node) - sub := tab.nodeFeed.Subscribe(ch) + if notify == nil { + // Lazily init the subscription. Do this while holding the + // lock so we don't miss any events that change the node count. + sub := initsub() defer sub.Unsubscribe() } tab.mutex.Unlock() - // Wait for a node add event. - select { - case <-ch: - case <-ctx.Done(): - return ctx.Err() - case <-tab.closeReq: - return errClosed + // Wait for table event. + if _, ok := <-notify; !ok { + break } } + return notifyErr } diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index c3b71ea5a6..a16b4d9cab 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -17,6 +17,7 @@ package discover import ( + "context" "crypto/ecdsa" "fmt" "math/rand" @@ -550,6 +551,45 @@ func TestSetFallbackNodes_DNSHostname(t *testing.T) { t.Logf("resolved localhost to %v", resolved.IPAddr()) } +// This test checks that waitForNodes does not block addFoundNode. +// See https://github.com/ethereum/go-ethereum/issues/34881. +func TestTable_waitForNodesLocking(t *testing.T) { + transport := newPingRecorder() + tab, db := newTestTable(transport, Config{}) + defer db.Close() + defer tab.close() + <-tab.initDone + + // waitForNodes will never reach this count, so it stays subscribed + // to nodeFeed and looping for the duration of the test. + waitCtx, cancelWait := context.WithCancel(context.Background()) + defer cancelWait() + waitDone := make(chan struct{}) + go func() { + defer close(waitDone) + tab.waitForNodes(waitCtx, 1<<20) + }() + + // Call addFoundNode in loop to send to the feed. + addDone := make(chan struct{}) + go func() { + defer close(addDone) + for i := range 10000 { + d := 240 + (i % 17) + n := nodeAtDistance(tab.self().ID(), d, intIP(i)) + tab.addFoundNode(n, true) + } + }() + + select { + case <-addDone: + cancelWait() + <-waitDone + case <-time.After(10 * time.Second): + t.Fatal("deadlock detected: add loop did not finish within 10s") + } +} + func newkey() *ecdsa.PrivateKey { key, err := crypto.GenerateKey() if err != nil { diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index 9e824dae18..ae8cbec3e2 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -447,6 +447,7 @@ func (t *UDPv4) loop() { // Start the timer so it fires when the next pending reply has expired. now := time.Now() for p, el := range iterList[*replyMatcher](plist) { + nextTimeout = p if dist := p.deadline.Sub(now); dist < 2*respTimeout { timeout.Reset(dist) return @@ -454,7 +455,7 @@ func (t *UDPv4) loop() { // Remove pending replies whose deadline is too far in the // future. These can occur if the system clock jumped // backwards after the deadline was assigned. - nextTimeout.errc <- errClockWarp + p.errc <- errClockWarp plist.Remove(el) } nextTimeout = nil @@ -554,8 +555,9 @@ func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) { if err := t.handlePacket(from, buf[:nbytes]); err != nil && unhandled == nil { t.log.Debug("Bad discv4 packet", "addr", from, "err", err) } else if err != nil && unhandled != nil { + p := ReadPacket{bytes.Clone(buf[:nbytes]), from} select { - case unhandled <- ReadPacket{buf[:nbytes], from}: + case unhandled <- p: default: } } diff --git a/params/protocol_params.go b/params/protocol_params.go index 9da275c486..3e36b83547 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -186,6 +186,16 @@ const ( HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935. MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block + + // BALItemCost is the gas-cost divisor for the EIP-7928 block access list + // size constraint: bal_items <= block_gas_limit / BALItemCost, where + // bal_items counts every distinct address in the BAL plus every storage + // key (writes + reads) carried by those accounts. + // + // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so + // the bound has a small safety margin for system-contract accesses that + // don't consume block gas. + BALItemCost uint64 = 2000 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation diff --git a/signer/core/api.go b/signer/core/api.go index 12acf925f0..3b7b53a312 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -196,8 +196,9 @@ func MetadataFromContext(ctx context.Context) Metadata { if info.Transport != "" { if info.Transport == "http" { m.Scheme = info.HTTP.Version + } else { + m.Scheme = info.Transport } - m.Scheme = info.Transport } if info.RemoteAddr != "" { m.Remote = info.RemoteAddr diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index c62b513145..d8b6ef0674 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "context" "encoding/json" "errors" @@ -309,7 +310,8 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hex if sig[64] != 27 && sig[64] != 28 { return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)") } - sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 + sig = bytes.Clone(sig) // Avoid mutating the input + sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 hash := accounts.TextHash(data) rpk, err := crypto.SigToPub(hash, sig) if err != nil { diff --git a/tests/state_test.go b/tests/state_test.go index 8444d211cf..cf1d4bce4c 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -35,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers/logger" - "github.com/holiman/uint256" ) func initMatcher(st *testMatcher) { @@ -329,7 +328,7 @@ func runBenchmark(b *testing.B, t *StateTest) { initialGas := vm.NewGasBudget(msg.GasLimit) // Execute the message. - _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), uint256.MustFromBig(msg.Value)) + _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value) if err != nil { b.Error(err) return diff --git a/tests/state_test_util.go b/tests/state_test_util.go index f7cf1c0697..e33e15fc8c 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -479,15 +479,15 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess From: from, To: to, Nonce: tx.Nonce, - Value: value, + Value: uint256.MustFromBig(value), GasLimit: gasLimit, - GasPrice: gasPrice, - GasFeeCap: tx.MaxFeePerGas, - GasTipCap: tx.MaxPriorityFeePerGas, + GasPrice: uint256.MustFromBig(gasPrice), + GasFeeCap: uint256.MustFromBig(tx.MaxFeePerGas), + GasTipCap: uint256.MustFromBig(tx.MaxPriorityFeePerGas), Data: data, AccessList: accessList, BlobHashes: tx.BlobVersionedHashes, - BlobGasFeeCap: tx.BlobGasFeeCap, + BlobGasFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap), SetCodeAuthorizations: authList, } return msg, nil diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 8b8d0357bf..572c109f1e 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic gas - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return } @@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data()) + floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return } diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go index e7f57d45a2..3516bf6bd5 100644 --- a/trie/bintrie/binary_node.go +++ b/trie/bintrie/binary_node.go @@ -27,8 +27,19 @@ const ( NodeTypeBytes = 1 // Size of node type prefix in serialization HashSize = 32 // Size of a hash in bytes StemBitmapSize = 32 // Size of the bitmap in a stem node (256 values = 32 bytes) + + MaxGroupDepth = 8 ) +// bitmapSizeForDepth returns the bitmap size in bytes for a given group depth. +// For depths 1-3, returns 1 byte. For depths 4-8, returns 2^(depth-3) bytes. +func bitmapSizeForDepth(groupDepth int) int { + if groupDepth <= 3 { + return 1 + } + return 1 << (groupDepth - 3) +} + const ( nodeTypeStem = iota + 1 nodeTypeInternal diff --git a/trie/bintrie/binary_node_test.go b/trie/bintrie/binary_node_test.go index 12ac199903..857060a0c0 100644 --- a/trie/bintrie/binary_node_test.go +++ b/trie/bintrie/binary_node_test.go @@ -23,8 +23,8 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// TestSerializeDeserializeInternalNode tests flat 65-byte serialization and -// deserialization of InternalNode through nodeStore. +// TestSerializeDeserializeInternalNode tests grouped serialization and +// deserialization of InternalNode through nodeStore at groupDepth=1. func TestSerializeDeserializeInternalNode(t *testing.T) { leftHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") rightHash := common.HexToHash("0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321") @@ -39,24 +39,32 @@ func TestSerializeDeserializeInternalNode(t *testing.T) { rootNode.right = rightRef s.root = rootRef - // Serialize the node — flat 65-byte format - serialized := s.serializeNode(rootRef) + // Serialize the node — grouped format at groupDepth=1: + // [type(1)][groupDepth(1)][bitmap(1)][leftHash(32)][rightHash(32)] = 67 bytes + serialized := s.serializeNode(rootRef, 1) - // Check the serialized format: [type(1)][leftHash(32)][rightHash(32)] if serialized[0] != nodeTypeInternal { t.Errorf("Expected type byte to be %d, got %d", nodeTypeInternal, serialized[0]) } + if serialized[1] != 1 { + t.Errorf("Expected groupDepth byte to be 1, got %d", serialized[1]) + } - expectedLen := NodeTypeBytes + 2*HashSize // 1 + 64 = 65 + expectedLen := NodeTypeBytes + 1 + 1 + 2*HashSize // type + groupDepth + bitmap + 2 hashes = 67 if len(serialized) != expectedLen { t.Errorf("Expected serialized length to be %d, got %d", expectedLen, len(serialized)) } - // Check that left and right hashes are embedded directly - if !bytes.Equal(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], leftHash[:]) { + // Both children present at a 1-level group → bitmap byte = 0b11000000. + if serialized[2] != 0xc0 { + t.Errorf("Expected bitmap byte 0xc0, got 0x%02x", serialized[2]) + } + + hashesStart := NodeTypeBytes + 1 + 1 + if !bytes.Equal(serialized[hashesStart:hashesStart+HashSize], leftHash[:]) { t.Error("Left hash not found at expected position") } - if !bytes.Equal(serialized[NodeTypeBytes+HashSize:], rightHash[:]) { + if !bytes.Equal(serialized[hashesStart+HashSize:], rightHash[:]) { t.Error("Right hash not found at expected position") } @@ -116,7 +124,7 @@ func TestSerializeDeserializeStemNode(t *testing.T) { } // Serialize the node - serialized := s.serializeNode(ref) + serialized := s.serializeNode(ref, 8) // Check the serialized format if serialized[0] != nodeTypeStem { @@ -195,8 +203,9 @@ func TestDeserializeInvalidType(t *testing.T) { // TestDeserializeInvalidLength tests deserialization with invalid data length. func TestDeserializeInvalidLength(t *testing.T) { s := newNodeStore() - // InternalNode with valid type byte but wrong length (needs exactly 65 bytes) - invalidData := []byte{nodeTypeInternal, 0, 0, 0} + // InternalNode group header with groupDepth=1 (valid) and a 1-byte bitmap + // announcing two present hashes, but the hash payload is missing. + invalidData := []byte{nodeTypeInternal, 1, 0xc0} _, err := s.deserializeNode(invalidData, 0) if err == nil { @@ -208,6 +217,21 @@ func TestDeserializeInvalidLength(t *testing.T) { } } +// TestDeserializeInvalidGroupDepth tests deserialization when the group depth +// byte is out of the supported 1..MaxGroupDepth range. +func TestDeserializeInvalidGroupDepth(t *testing.T) { + s := newNodeStore() + invalidData := []byte{nodeTypeInternal, 0, 0, 0} + + _, err := s.deserializeNode(invalidData, 0) + if err == nil { + t.Fatal("Expected error for invalid group depth, got nil") + } + if err.Error() != "invalid group depth" { + t.Errorf("Expected 'invalid group depth' error, got: %v", err) + } +} + // TestKeyToPath tests the keyToPath function. func TestKeyToPath(t *testing.T) { tests := []struct { diff --git a/trie/bintrie/hashed_node_test.go b/trie/bintrie/hashed_node_test.go index ae77b7c570..2e12bfba5e 100644 --- a/trie/bintrie/hashed_node_test.go +++ b/trie/bintrie/hashed_node_test.go @@ -95,7 +95,7 @@ func TestHashedNodeInsertValuesAtStem(t *testing.T) { sn.setValue(byte(i), v) } } - serialized := rs.serializeNode(ref) + serialized := rs.serializeNode(ref, 8) validResolver := func(path []byte, hash common.Hash) ([]byte, error) { return serialized, nil diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go index 8d5a75de8c..4d8da8af37 100644 --- a/trie/bintrie/internal_node_test.go +++ b/trie/bintrie/internal_node_test.go @@ -90,7 +90,7 @@ func TestInternalNodeGetWithResolver(t *testing.T) { ref := rs.newStemRef(stem, 1) sn := rs.getStem(ref.Index()) sn.setValue(5, common.HexToHash("0xabcd").Bytes()) - return rs.serializeNode(ref), nil + return rs.serializeNode(ref, 8), nil } return nil, errors.New("node not found") } @@ -290,10 +290,7 @@ func TestInternalNodeCollectNodes(t *testing.T) { collectedPaths = append(collectedPaths, pathCopy) } - err := s.collectNodes(s.root, []byte{1}, flushFn) - if err != nil { - t.Fatalf("Failed to collect nodes: %v", err) - } + s.collectNodes(s.root, []byte{1}, flushFn, 8) // Should have collected 3 nodes: left stem, right stem, and the internal node itself if len(collectedPaths) != 3 { diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go index 31645430c3..a920f91378 100644 --- a/trie/bintrie/iterator.go +++ b/trie/bintrie/iterator.go @@ -205,7 +205,7 @@ func (it *binaryNodeIterator) Path() []byte { } func (it *binaryNodeIterator) NodeBlob() []byte { - return it.store.serializeNode(it.current) + return it.store.serializeNode(it.current, it.trie.groupDepth) } // Leaf reports whether the iterator is currently positioned at a leaf value. diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go index 5faf903fba..ae6b57ab34 100644 --- a/trie/bintrie/stem_node_test.go +++ b/trie/bintrie/stem_node_test.go @@ -320,10 +320,7 @@ func TestStemNodeCollectNodes(t *testing.T) { collectedPaths = append(collectedPaths, pathCopy) } - err := s.collectNodes(s.root, []byte{0, 1, 0}, flushFn) - if err != nil { - t.Fatalf("Failed to collect nodes: %v", err) - } + s.collectNodes(s.root, []byte{0, 1, 0}, flushFn, 8) // Should have collected one node (itself) if len(collectedPaths) != 1 { diff --git a/trie/bintrie/store_commit.go b/trie/bintrie/store_commit.go index 7101087b51..b14bffbc6c 100644 --- a/trie/bintrie/store_commit.go +++ b/trie/bintrie/store_commit.go @@ -107,18 +107,83 @@ func (s *nodeStore) hashInternal(idx uint32) common.Hash { return node.hash } -// SerializeNode serializes a node into the flat on-disk format. -func (s *nodeStore) serializeNode(ref nodeRef) []byte { +// serializeSubtree recursively collects child hashes from a subtree of InternalNodes. +// It traverses up to `remainingDepth` levels, storing hashes of bottom-layer children. +// position tracks the current index (0 to 2^groupDepth - 1) for bitmap placement. +// hashes collects the hashes of present children, bitmap tracks which positions are present. +func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position int, absoluteDepth int, bitmap []byte, hashes *[]common.Hash) { + if remainingDepth == 0 { + // Bottom layer: store hash if not empty + switch ref.Kind() { + case kindEmpty: + // Leave bitmap bit unset, don't add hash + return + default: + // StemNode, HashedNode, or InternalNode at boundary: store hash + bitmap[position/8] |= 1 << (7 - (position % 8)) + *hashes = append(*hashes, s.computeHash(ref)) + } + return + } + switch ref.Kind() { case kindInternal: + leftPos := position * 2 + rightPos := position*2 + 1 + s.serializeSubtree(s.getInternal(ref.Index()).left, remainingDepth-1, leftPos, absoluteDepth+1, bitmap, hashes) + s.serializeSubtree(s.getInternal(ref.Index()).right, remainingDepth-1, rightPos, absoluteDepth+1, bitmap, hashes) + case kindEmpty: + return + default: + // StemNode or HashedNode encountered before reaching the group's bottom + // layer. Compute the leaf bitmap position where this node's hash will + // be stored. + leafPos := position + switch ref.Kind() { + case kindStem: + sn := s.getStem(ref.Index()) + // Extend position using the stem's key bits so that + // GetValuesAtStem traversal (which follows key bits) finds the hash. + for d := 0; d < remainingDepth; d++ { + bit := sn.Stem[(absoluteDepth+d)/8] >> (7 - ((absoluteDepth + d) % 8)) & 1 + leafPos = leafPos*2 + int(bit) + } + default: + // HashedNode or unknown: extend all-left (no key bits available). + // This matches the all-zero path that resolveNode would follow. + leafPos = position << remainingDepth + } + bitmap[leafPos/8] |= 1 << (7 - (leafPos % 8)) + *hashes = append(*hashes, s.computeHash(ref)) + } +} + +// SerializeNode serializes a node into the flat on-disk format. +func (s *nodeStore) serializeNode(ref nodeRef, groupDepth int) []byte { + switch ref.Kind() { + case kindInternal: + // InternalNode group: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes + bitmapSize := bitmapSizeForDepth(groupDepth) + bitmap := make([]byte, bitmapSize) + var hashes []common.Hash + node := s.getInternal(ref.Index()) - var serialized [NodeTypeBytes + HashSize + HashSize]byte + s.serializeSubtree(ref, groupDepth, 0, int(node.depth), bitmap, &hashes) + + // Build serialized output + serializedLen := NodeTypeBytes + 1 + bitmapSize + len(hashes)*HashSize + serialized := make([]byte, serializedLen) serialized[0] = nodeTypeInternal - lh := s.computeHash(node.left) - rh := s.computeHash(node.right) - copy(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], lh[:]) - copy(serialized[NodeTypeBytes+HashSize:], rh[:]) - return serialized[:] + serialized[1] = byte(groupDepth) // group depth => bitmap size for a sparse group + copy(serialized[2:2+bitmapSize], bitmap) + + offset := NodeTypeBytes + 1 + bitmapSize + for _, h := range hashes { + copy(serialized[offset:offset+HashSize], h.Bytes()) + offset += HashSize + } + + return serialized case kindStem: sn := s.getStem(ref.Index()) @@ -163,6 +228,59 @@ func (s *nodeStore) deserializeNodeWithHash(serialized []byte, depth int, hn com return s.decodeNode(serialized, depth, hn, false, false) } +// deserializeSubtree reconstructs an InternalNode subtree from grouped serialization. +// remainingDepth is how many more levels to build, position is current index in the bitmap, +// nodeDepth is the actual trie depth for the node being created. +// hashIdx tracks the current position in the hash data (incremented as hashes are consumed). +func (s *nodeStore) deserializeSubtree(hn common.Hash, remainingDepth int, position int, nodeDepth int, bitmap []byte, hashData []byte, hashIdx *int, mustRecompute bool, dirty bool) (nodeRef, error) { + if remainingDepth == 0 { + // Bottom layer: check bitmap and return HashedNode or Empty + if bitmap[position/8]>>(7-(position%8))&1 == 1 { + if len(hashData) < (*hashIdx+1)*HashSize { + return emptyRef, errInvalidSerializedLength + } + hash := common.BytesToHash(hashData[*hashIdx*HashSize : (*hashIdx+1)*HashSize]) + *hashIdx++ + return s.newHashedRef(hash), nil + } + return emptyRef, nil + } + + // Check if this entire subtree is empty by examining all relevant bitmap bits + leftPos := position * 2 + rightPos := position*2 + 1 + + // note that the parent might not need root computations, but the children + // do, because their hash isn't saved. Hence `mustRecompute` is set to `true`. + left, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, leftPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty) + if err != nil { + return emptyRef, err + } + right, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, rightPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty) + if err != nil { + return emptyRef, err + } + + // If both children are empty, return Empty + if left.IsEmpty() && right.IsEmpty() { + return emptyRef, nil + } + + ref := s.newInternalRef(nodeDepth) + node := s.getInternal(ref.Index()) + node.left = left + node.right = right + node.mustRecompute = mustRecompute + if !mustRecompute { + // mustRecompute will only be false for the root of the subtree, + // for which we already know the hash. + node.hash = hn + node.mustRecompute = false + } + node.dirty = dirty + return ref, nil +} + func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mustRecompute, dirty bool) (nodeRef, error) { if len(serialized) == 0 { return emptyRef, nil @@ -170,31 +288,23 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus switch serialized[0] { case nodeTypeInternal: - if len(serialized) != NodeTypeBytes+2*HashSize { + // Grouped format: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes + if len(serialized) < NodeTypeBytes+1 { return emptyRef, errInvalidSerializedLength } - var leftHash, rightHash common.Hash - copy(leftHash[:], serialized[NodeTypeBytes:NodeTypeBytes+HashSize]) - copy(rightHash[:], serialized[NodeTypeBytes+HashSize:]) + groupDepth := int(serialized[1]) + if groupDepth < 1 || groupDepth > MaxGroupDepth { + return 0, errors.New("invalid group depth") + } + bitmapSize := bitmapSizeForDepth(groupDepth) + if len(serialized) < NodeTypeBytes+1+bitmapSize { + return 0, errInvalidSerializedLength + } + bitmap := serialized[2 : 2+bitmapSize] + hashData := serialized[2+bitmapSize:] - var leftRef, rightRef nodeRef - if leftHash != (common.Hash{}) { - leftRef = s.newHashedRef(leftHash) - } - if rightHash != (common.Hash{}) { - rightRef = s.newHashedRef(rightHash) - } - - ref := s.newInternalRef(depth) - node := s.getInternal(ref.Index()) - node.left = leftRef - node.right = rightRef - if !mustRecompute { - node.hash = hn - node.mustRecompute = false - } - node.dirty = dirty - return ref, nil + hashIdx := 0 + return s.deserializeSubtree(hn, groupDepth, 0, depth, bitmap, hashData, &hashIdx, mustRecompute, dirty) case nodeTypeStem: if len(serialized) < NodeTypeBytes+StemSize+StemBitmapSize { @@ -230,45 +340,112 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus // CollectNodes flushes every node that needs flushing via flushfn in post-order. // Invariant: any ancestor of a node that needs flushing is itself marked, so a // clean root means the whole subtree is clean. -func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) error { +func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn, groupDepth int) { switch ref.Kind() { - case kindEmpty: - return nil case kindInternal: node := s.getInternal(ref.Index()) if !node.dirty { - return nil + return } - // Reuse path buffer across children: flushfn consumers - // (NodeSet.AddNode, tracer.Get) clone via string(path), so in-place - // mutation is safe. - path = append(path, 0) - if err := s.collectNodes(node.left, path, flushfn); err != nil { - return err + // Only flush at group boundaries (depth % groupDepth == 0) + if int(node.depth)%groupDepth == 0 { + // We're at a group boundary - first collect any nodes in deeper groups, + // then flush this group + s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-1) + flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth)) + node.dirty = false + return } - path[len(path)-1] = 1 - if err := s.collectNodes(node.right, path, flushfn); err != nil { - return err - } - path = path[:len(path)-1] - flushfn(path, s.computeHash(ref), s.serializeNode(ref)) - node.dirty = false - return nil + // Not at a group boundary - this shouldn't happen if we're called correctly from root + // but handle it by continuing to traverse + s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-(int(node.depth)%groupDepth)-1) case kindStem: sn := s.getStem(ref.Index()) if !sn.dirty { - return nil + return } - flushfn(path, s.computeHash(ref), s.serializeNode(ref)) + flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth)) sn.dirty = false - return nil - case kindHashed: - return nil // Already committed + case kindHashed, kindEmpty: default: - return fmt.Errorf("CollectNodes: unexpected kind %d", ref.Kind()) + panic(fmt.Sprintf("CollectNodes: unexpected kind %d", ref.Kind())) } } +// collectChildGroups traverses within a group to find and collect nodes in the next group. +// remainingLevels is how many more levels below the current node until we reach the group boundary. +// When remainingLevels=0, the current node's children are at the next group boundary. +func (s *nodeStore) collectChildGroups(node *InternalNode, path []byte, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error { + if remainingLevels == 0 { + // Current node is at depth (groupBoundary - 1), its children are at the next group boundary + if !node.left.IsEmpty() { + s.collectNodes(node.left, appendBit(path, 0), flushfn, groupDepth) + } + if !node.right.IsEmpty() { + s.collectNodes(node.right, appendBit(path, 1), flushfn, groupDepth) + } + return nil + } + + if !node.left.IsEmpty() { + switch node.left.Kind() { + case kindInternal: + n := s.getInternal(node.left.Index()) + if err := s.collectChildGroups(n, appendBit(path, 0), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 0), node.left, remainingLevels) + s.collectNodes(node.left, extPath, flushfn, groupDepth) + } + } + if !node.right.IsEmpty() { + switch node.right.Kind() { + case kindInternal: + n := s.getInternal(node.right.Index()) + if err := s.collectChildGroups(n, appendBit(path, 1), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 1), node.right, remainingLevels) + s.collectNodes(node.right, extPath, flushfn, groupDepth) + } + } + return nil +} + +// extendPathToGroupLeaf extends a storage path to the group's leaf boundary, +// matching the projection done by serializeSubtree. For StemNodes, the path +// is extended using the stem's key bits (same as serializeSubtree). For other +// node types, the path is extended with all-zero (left) bits. +func (s *nodeStore) extendPathToGroupLeaf(path []byte, node nodeRef, remainingLevels int) []byte { + if remainingLevels <= 0 { + return path + } + if node.Kind() == kindStem { + sn := s.getStem(node.Index()) + for _ = range remainingLevels { + bit := sn.Stem[len(path)/8] >> (7 - (len(path) % 8)) & 1 + path = appendBit(path, bit) + } + } else { + // HashedNode or other: all-left extension (matches serializeSubtree's + // position << remainingDepth behavior). + for _ = range remainingLevels { + path = appendBit(path, 0) + } + } + return path +} + +// appendBit appends a bit to a path, returning a new slice +func appendBit(path []byte, bit byte) []byte { + var p [256]byte + copy(p[:], path) + result := p[:len(path)] + return append(result, bit) +} + func (s *nodeStore) toDot(ref nodeRef, parent, path string) string { switch ref.Kind() { case kindInternal: diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 8c69e0aa00..e3436e3df1 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -107,9 +107,14 @@ func ChunkifyCode(code []byte) ChunkedCode { // BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864. type BinaryTrie struct { - store *nodeStore - reader *trie.Reader - tracer *trie.PrevalueTracer + store *nodeStore + reader *trie.Reader + tracer *trie.PrevalueTracer + groupDepth int // Number of levels per serialized group (1-8, default 8) +} + +func (t *BinaryTrie) GroupDepth() int { + return t.groupDepth } // ToDot converts the binary trie to a DOT language representation. Useful for debugging. @@ -119,15 +124,20 @@ func (t *BinaryTrie) ToDot() string { } // NewBinaryTrie creates a new binary trie. -func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) { +// groupDepth specifies the number of levels per serialized group (1-8). +func NewBinaryTrie(root common.Hash, db database.NodeDatabase, groupDepth int) (*BinaryTrie, error) { + if groupDepth < 1 || groupDepth > MaxGroupDepth { + panic("invalid group depth size") + } reader, err := trie.NewReader(root, common.Hash{}, db) if err != nil { return nil, err } t := &BinaryTrie{ - store: newNodeStore(), - reader: reader, - tracer: trie.NewPrevalueTracer(), + store: newNodeStore(), + reader: reader, + tracer: trie.NewPrevalueTracer(), + groupDepth: groupDepth, } // Parse the root node if it's not empty if root != types.EmptyBinaryHash && root != types.EmptyRootHash { @@ -312,12 +322,9 @@ func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) { // Pre-size the path buffer: collectNodes reuses it in-place via // append/truncate; 32 covers typical binary-trie depth without regrowth. pathBuf := make([]byte, 0, 32) - err := t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) { + t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) { nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path))) - }) - if err != nil { - panic(fmt.Errorf("CollectNodes failed: %v", err)) - } + }, t.groupDepth) return t.Hash(), nodeset } @@ -341,9 +348,10 @@ func (t *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // Copy creates a deep copy of the trie. func (t *BinaryTrie) Copy() *BinaryTrie { return &BinaryTrie{ - store: t.store.Copy(), - reader: t.reader, - tracer: t.tracer.Copy(), + store: t.store.Copy(), + reader: t.reader, + tracer: t.tracer.Copy(), + groupDepth: t.groupDepth, } } diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go index 73aacb76c4..8b7d9e46d6 100644 --- a/trie/bintrie/trie_test.go +++ b/trie/bintrie/trie_test.go @@ -768,8 +768,9 @@ func TestGetStorageNonMembershipInternalRoot(t *testing.T) { // flushes only the root-to-leaf path. func TestCommitSkipCleanSubtrees(t *testing.T) { tr := &BinaryTrie{ - store: newNodeStore(), - tracer: trie.NewPrevalueTracer(), + store: newNodeStore(), + tracer: trie.NewPrevalueTracer(), + groupDepth: 1, } const n = 200 key := func(i int) [HashSize]byte { diff --git a/triedb/database.go b/triedb/database.go index 533097c9e3..ef95169df1 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -31,12 +31,15 @@ import ( // Config defines all necessary options for database. type Config struct { - Preimages bool // Flag whether the preimage of node key is recorded - IsUBT bool // Flag whether the db is holding a verkle tree - HashDB *hashdb.Config // Configs for hash-based scheme - PathDB *pathdb.Config // Configs for experimental path-based scheme + Preimages bool // Flag whether the preimage of node key is recorded + IsUBT bool // Flag whether the db is holding a unified binary tree + BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8, default 8) + HashDB *hashdb.Config // Configs for hash-based scheme + PathDB *pathdb.Config // Configs for experimental path-based scheme } +const DefaultBinTrieGroupDepth = 5 + // HashDefaults represents a config for using hash-based scheme with // default settings. var HashDefaults = &Config{ @@ -45,12 +48,13 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } -// UBTDefaults represents a config for holding verkle trie data +// UBTDefaults represents a config for holding unified binary trie data // using path-based scheme with default settings. var UBTDefaults = &Config{ - Preimages: false, - IsUBT: true, - PathDB: pathdb.Defaults, + Preimages: false, + IsUBT: true, + BinTrieGroupDepth: DefaultBinTrieGroupDepth, + PathDB: pathdb.Defaults, } // backend defines the methods needed to access/update trie nodes in different @@ -323,6 +327,16 @@ func (db *Database) Enable(root common.Hash) error { return pdb.Enable(root) } +// AdoptSyncedState activates the database after a snap/2 sync and adopts the +// flat state populated during sync as-is, skipping regeneration. +func (db *Database) AdoptSyncedState(root common.Hash) error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return errors.New("not supported") + } + return pdb.AdoptSyncedState(root) +} + // Journal commits an entire diff hierarchy to disk into a single journal entry. // This is meant to be used during shutdown to persist the snapshot without // flattening everything down (bad for reorgs). It's only supported by path-based @@ -393,3 +407,7 @@ func (db *Database) SnapshotCompleted() bool { } return pdb.SnapshotCompleted() } + +func (db *Database) BinTrieGroupDepth() int { + return db.config.BinTrieGroupDepth +} diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 98975d7fa5..e52949c93e 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -365,16 +365,9 @@ func (db *Database) Disable() error { return nil } -// Enable activates database and resets the state tree with the provided persistent -// state root once the state sync is finished. -func (db *Database) Enable(root common.Hash) error { - db.lock.Lock() - defer db.lock.Unlock() - - // Short circuit if the database is in read only mode. - if db.readOnly { - return errDatabaseReadOnly - } +// resetForReactivation performs the pathdb-side bookkeeping shared by both +// Enable and AdoptSyncedState. +func (db *Database) resetForReactivation(root common.Hash) error { // Ensure the provided state root matches the stored one. stored, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) if err != nil { @@ -383,27 +376,40 @@ func (db *Database) Enable(root common.Hash) error { if stored != root { return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root) } - // Drop the stale state journal in persistent database and - // reset the persistent state id back to zero. + // Drop the stale state journal marker and reset the persistent state id + // back to zero. batch := db.diskdb.NewBatch() rawdb.DeleteSnapshotRoot(batch) rawdb.WritePersistentStateID(batch, 0) if err := batch.Write(); err != nil { return err } - // Clean up all state histories in freezer. Theoretically - // all root->id mappings should be removed as well. Since - // mappings can be huge and might take a while to clear - // them, just leave them in disk and wait for overwriting. + // Clean up all state histories in the freezer. Theoretically all root->id + // mappings should be removed as well; since those can be huge, leave them + // on disk and let them be overwritten. purgeHistory(db.stateFreezer, db.diskdb, typeStateHistory) purgeHistory(db.trienodeFreezer, db.diskdb, typeTrienodeHistory) - // Re-enable the database as the final step. + // Re-enable the database as the final bookkeeping step. db.waitSync = false rawdb.WriteSnapSyncStatusFlag(db.diskdb, rawdb.StateSyncFinished) + return nil +} - // Re-construct a new disk layer backed by persistent state - // and schedule the state snapshot generation if it's permitted. +// Enable activates the database after a snap/1 sync and schedules background +// regeneration of the snapshot from the trie. +func (db *Database) Enable(root common.Hash) error { + db.lock.Lock() + defer db.lock.Unlock() + + if db.readOnly { + return errDatabaseReadOnly + } + if err := db.resetForReactivation(root); err != nil { + return err + } + // Re-construct a new disk layer backed by persistent state and schedule + // the state snapshot generation if it's permitted. db.tree.init(generateSnapshot(db, root, db.isUBT || db.config.SnapshotNoBuild)) // After snap sync, the state of the database may have changed completely. @@ -416,6 +422,43 @@ func (db *Database) Enable(root common.Hash) error { return nil } +// AdoptSyncedState reactivates the database after a snap/2 sync. The syncer +// already wrote a consistent flat state, so we take it as-is instead of +// rebuilding it from the trie. The new disk layer has no generator attached, +// and a "done" marker is written so future boots know the snapshot is +// already complete. +func (db *Database) AdoptSyncedState(root common.Hash) error { + db.lock.Lock() + defer db.lock.Unlock() + + if db.readOnly { + return errDatabaseReadOnly + } + if err := db.resetForReactivation(root); err != nil { + return err + } + + // Tell the snapshot subsystem the flat state is good by writing the new root + // and a "done" marker (nil journal) so the next boot doesn't try to rebuild it. + batch := db.diskdb.NewBatch() + rawdb.WriteSnapshotRoot(batch, root) + journalProgress(batch, nil, nil) + if err := batch.Write(); err != nil { + return err + } + + // New disk layer, no generator attached. Nothing to rebuild, and reads + // can serve the flat state right away without waiting on a generator to + // scan past every key. + dl := newDiskLayer(root, 0, db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0), nil) + db.tree.init(dl) + + db.setHistoryIndexer() + + log.Info("Adopted synced state", "root", root) + return nil +} + // Recover rollbacks the database to a specified historical point. // The state is supported as the rollback destination only if it's // canonical state and the corresponding trie histories are existent. diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index 8ceb22eaba..41212dc9d0 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -748,6 +748,84 @@ func TestDisable(t *testing.T) { } } +// TestAdoptSyncedState verifies that AdoptSyncedState rejects a wrong root, +// writes the on-disk markers that say the snapshot is already complete, +// leaves a single fresh disk layer with no generator attached, and clears +// out stale state histories. +func TestAdoptSyncedState(t *testing.T) { + maxDiffLayers = 4 + defer func() { + maxDiffLayers = 128 + }() + + tester := newTester(t, &testerConfig{layers: 12}) + defer tester.release() + + // Push everything down to disk so the trie root is the persistent root. + if err := tester.db.Commit(tester.lastHash(), false); err != nil { + t.Fatalf("Failed to commit, err: %v", err) + } + stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) + + // Mimic the snap-syncing state. + if err := tester.db.Disable(); err != nil { + t.Fatalf("Failed to disable database: %v", err) + } + // Mismatched root must be rejected. + if err := tester.db.AdoptSyncedState(types.EmptyRootHash); err == nil { + t.Fatal("Mismatched root should be rejected") + } + if err := tester.db.AdoptSyncedState(stored); err != nil { + t.Fatalf("AdoptSyncedState failed: %v", err) + } + + // On-disk markers reflect a completed snapshot. + if got := rawdb.ReadSnapshotRoot(tester.db.diskdb); got != stored { + t.Fatalf("SnapshotRoot mismatch: got %x want %x", got, stored) + } + if blob := rawdb.ReadSnapshotGenerator(tester.db.diskdb); len(blob) == 0 { + t.Fatal("Generator journal not written") + } else { + var entry journalGenerator + if err := rlp.DecodeBytes(blob, &entry); err != nil { + t.Fatalf("Failed to decode generator journal: %v", err) + } + if !entry.Done { + t.Fatal("Generator journal should be marked Done") + } + // RLP turns a nil slice into an empty one on decode, so check length. + if len(entry.Marker) != 0 { + t.Fatalf("Generator marker should be empty, got %x", entry.Marker) + } + } + if rawdb.ReadSnapSyncStatusFlag(tester.db.diskdb) != rawdb.StateSyncFinished { + t.Fatal("Sync-status flag should be StateSyncFinished") + } + if tester.db.waitSync { + t.Fatal("waitSync should be false after adopt") + } + + // State histories are purged. + if n, err := tester.db.stateFreezer.Ancients(); err != nil || n != 0 { + t.Fatalf("State histories not purged: count=%d err=%v", n, err) + } + + // Layer tree has a single disk layer with no generator attached. + if got := tester.db.tree.len(); got != 1 { + t.Fatalf("Expected single layer, got %d", got) + } + dl := tester.db.tree.bottom() + if dl.rootHash() != stored { + t.Fatalf("Disk layer root mismatch: got %x want %x", dl.rootHash(), stored) + } + if dl.generator != nil { + t.Fatal("Disk layer should have no generator after adopt") + } + if dl.genMarker() != nil { + t.Fatal("genMarker should be nil after adopt") + } +} + func TestCommit(t *testing.T) { // Redefine the diff layer depth allowance for faster testing. maxDiffLayers = 4 diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go index 2197e85272..191c2fadf5 100644 --- a/triedb/pathdb/iterator_test.go +++ b/triedb/pathdb/iterator_test.go @@ -489,7 +489,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) { if i%8 == 0 { e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) } - if i > 50 || i < 85 { + if i > 50 && i < 85 { f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) } if i%64 == 0 { diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go index efcc3f2549..657fbbff27 100644 --- a/triedb/pathdb/journal.go +++ b/triedb/pathdb/journal.go @@ -161,7 +161,19 @@ func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher) (*journalGenerator, // loadLayers loads a pre-existing state layer backed by a key-value store. func (db *Database) loadLayers() layer { // Retrieve the root node of persistent state. - root, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) + var ( + root common.Hash + err error + ) + if db.isUBT { + root = rawdb.ReadSnapshotRoot(db.diskdb) + if root == (common.Hash{}) { + root = types.EmptyBinaryHash + } + } else { + blob := rawdb.ReadAccountTrieNode(db.diskdb, nil) + root, err = db.hasher(blob) + } if err != nil { log.Crit("Failed to compute node hash", "err", err) } diff --git a/version/version.go b/version/version.go index ea1f5fc632..5d402f3009 100644 --- a/version/version.go +++ b/version/version.go @@ -19,6 +19,6 @@ package version const ( Major = 1 // Major version component of the current release Minor = 17 // Minor version component of the current release - Patch = 3 // Patch version component of the current release + Patch = 4 // Patch version component of the current release Meta = "unstable" // Version metadata to append to the version string )