diff --git a/accounts/abi/abigen/source2.go.tpl b/accounts/abi/abigen/source2.go.tpl index c517caf6f4..9285497511 100644 --- a/accounts/abi/abigen/source2.go.tpl +++ b/accounts/abi/abigen/source2.go.tpl @@ -59,6 +59,11 @@ var ( abi abi.ABI } + // GetABI returns the ABI associated with this contract binding. + func (c *{{.Type}}) GetABI() abi.ABI { + return c.abi + } + // New{{.Type}} creates a new instance of {{.Type}}. func New{{.Type}}() *{{.Type}} { parsed, err := {{.Type}}MetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/callbackparam.go.txt b/accounts/abi/abigen/testdata/v2/callbackparam.go.txt index d22b978486..a01b125acf 100644 --- a/accounts/abi/abigen/testdata/v2/callbackparam.go.txt +++ b/accounts/abi/abigen/testdata/v2/callbackparam.go.txt @@ -36,6 +36,11 @@ type CallbackParam struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *CallbackParam) GetABI() abi.ABI { + return c.abi +} + // NewCallbackParam creates a new instance of CallbackParam. func NewCallbackParam() *CallbackParam { parsed, err := CallbackParamMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt index f0bba246ab..66582aa5c6 100644 --- a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt +++ b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt @@ -36,6 +36,11 @@ type Crowdsale struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Crowdsale) GetABI() abi.ABI { + return c.abi +} + // NewCrowdsale creates a new instance of Crowdsale. func NewCrowdsale() *Crowdsale { parsed, err := CrowdsaleMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/dao.go.txt b/accounts/abi/abigen/testdata/v2/dao.go.txt index 0e9adba31e..a810c65bd9 100644 --- a/accounts/abi/abigen/testdata/v2/dao.go.txt +++ b/accounts/abi/abigen/testdata/v2/dao.go.txt @@ -36,6 +36,11 @@ type DAO struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DAO) GetABI() abi.ABI { + return c.abi +} + // NewDAO creates a new instance of DAO. func NewDAO() *DAO { parsed, err := DAOMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt b/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt index 302f1d736f..559ea83422 100644 --- a/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt +++ b/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt @@ -36,6 +36,11 @@ type DeeplyNestedArray struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DeeplyNestedArray) GetABI() abi.ABI { + return c.abi +} + // NewDeeplyNestedArray creates a new instance of DeeplyNestedArray. func NewDeeplyNestedArray() *DeeplyNestedArray { parsed, err := DeeplyNestedArrayMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/empty.go.txt b/accounts/abi/abigen/testdata/v2/empty.go.txt index 7082e20799..7deea15570 100644 --- a/accounts/abi/abigen/testdata/v2/empty.go.txt +++ b/accounts/abi/abigen/testdata/v2/empty.go.txt @@ -36,6 +36,11 @@ type Empty struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Empty) GetABI() abi.ABI { + return c.abi +} + // NewEmpty creates a new instance of Empty. func NewEmpty() *Empty { parsed, err := EmptyMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt index d0600d7c3e..213ab609f5 100644 --- a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt @@ -35,6 +35,11 @@ type EventChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *EventChecker) GetABI() abi.ABI { + return c.abi +} + // NewEventChecker creates a new instance of EventChecker. func NewEventChecker() *EventChecker { parsed, err := EventCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/getter.go.txt b/accounts/abi/abigen/testdata/v2/getter.go.txt index 69a4fb54c8..69267767c2 100644 --- a/accounts/abi/abigen/testdata/v2/getter.go.txt +++ b/accounts/abi/abigen/testdata/v2/getter.go.txt @@ -36,6 +36,11 @@ type Getter struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Getter) GetABI() abi.ABI { + return c.abi +} + // NewGetter creates a new instance of Getter. func NewGetter() *Getter { parsed, err := GetterMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt b/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt index e7301521f4..1d14a7109d 100644 --- a/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt +++ b/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt @@ -36,6 +36,11 @@ type IdentifierCollision struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *IdentifierCollision) GetABI() abi.ABI { + return c.abi +} + // NewIdentifierCollision creates a new instance of IdentifierCollision. func NewIdentifierCollision() *IdentifierCollision { parsed, err := IdentifierCollisionMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/inputchecker.go.txt b/accounts/abi/abigen/testdata/v2/inputchecker.go.txt index 1743f336db..4522aea52a 100644 --- a/accounts/abi/abigen/testdata/v2/inputchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/inputchecker.go.txt @@ -35,6 +35,11 @@ type InputChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *InputChecker) GetABI() abi.ABI { + return c.abi +} + // NewInputChecker creates a new instance of InputChecker. func NewInputChecker() *InputChecker { parsed, err := InputCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/interactor.go.txt b/accounts/abi/abigen/testdata/v2/interactor.go.txt index f33c95b639..3e88b4a521 100644 --- a/accounts/abi/abigen/testdata/v2/interactor.go.txt +++ b/accounts/abi/abigen/testdata/v2/interactor.go.txt @@ -36,6 +36,11 @@ type Interactor struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Interactor) GetABI() abi.ABI { + return c.abi +} + // NewInteractor creates a new instance of Interactor. func NewInteractor() *Interactor { parsed, err := InteractorMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt index 5e4a9ecaf0..13f96527ee 100644 --- a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt +++ b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt @@ -42,6 +42,11 @@ type NameConflict struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *NameConflict) GetABI() abi.ABI { + return c.abi +} + // NewNameConflict creates a new instance of NameConflict. func NewNameConflict() *NameConflict { parsed, err := NameConflictMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt index 0af31a1cfb..4f5392f19d 100644 --- a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt +++ b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt @@ -36,6 +36,11 @@ type NumericMethodName struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *NumericMethodName) GetABI() abi.ABI { + return c.abi +} + // NewNumericMethodName creates a new instance of NumericMethodName. func NewNumericMethodName() *NumericMethodName { parsed, err := NumericMethodNameMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/outputchecker.go.txt b/accounts/abi/abigen/testdata/v2/outputchecker.go.txt index f1c98f536e..8d3e475e98 100644 --- a/accounts/abi/abigen/testdata/v2/outputchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/outputchecker.go.txt @@ -35,6 +35,11 @@ type OutputChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *OutputChecker) GetABI() abi.ABI { + return c.abi +} + // NewOutputChecker creates a new instance of OutputChecker. func NewOutputChecker() *OutputChecker { parsed, err := OutputCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/overload.go.txt b/accounts/abi/abigen/testdata/v2/overload.go.txt index 563edf7842..006a2c7c3c 100644 --- a/accounts/abi/abigen/testdata/v2/overload.go.txt +++ b/accounts/abi/abigen/testdata/v2/overload.go.txt @@ -36,6 +36,11 @@ type Overload struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Overload) GetABI() abi.ABI { + return c.abi +} + // NewOverload creates a new instance of Overload. func NewOverload() *Overload { parsed, err := OverloadMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt b/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt index 296de1fccc..76c5d72d29 100644 --- a/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt +++ b/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt @@ -36,6 +36,11 @@ type RangeKeyword struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *RangeKeyword) GetABI() abi.ABI { + return c.abi +} + // NewRangeKeyword creates a new instance of RangeKeyword. func NewRangeKeyword() *RangeKeyword { parsed, err := RangeKeywordMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/slicer.go.txt b/accounts/abi/abigen/testdata/v2/slicer.go.txt index 379f136453..416a629cb7 100644 --- a/accounts/abi/abigen/testdata/v2/slicer.go.txt +++ b/accounts/abi/abigen/testdata/v2/slicer.go.txt @@ -36,6 +36,11 @@ type Slicer struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Slicer) GetABI() abi.ABI { + return c.abi +} + // NewSlicer creates a new instance of Slicer. func NewSlicer() *Slicer { parsed, err := SlicerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/structs.go.txt b/accounts/abi/abigen/testdata/v2/structs.go.txt index 4d8864cf6e..ee55ec5620 100644 --- a/accounts/abi/abigen/testdata/v2/structs.go.txt +++ b/accounts/abi/abigen/testdata/v2/structs.go.txt @@ -41,6 +41,11 @@ type Structs struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Structs) GetABI() abi.ABI { + return c.abi +} + // NewStructs creates a new instance of Structs. func NewStructs() *Structs { parsed, err := StructsMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/token.go.txt b/accounts/abi/abigen/testdata/v2/token.go.txt index 3bd60a6cdd..1bd364a918 100644 --- a/accounts/abi/abigen/testdata/v2/token.go.txt +++ b/accounts/abi/abigen/testdata/v2/token.go.txt @@ -36,6 +36,11 @@ type Token struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Token) GetABI() abi.ABI { + return c.abi +} + // NewToken creates a new instance of Token. func NewToken() *Token { parsed, err := TokenMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/tuple.go.txt b/accounts/abi/abigen/testdata/v2/tuple.go.txt index 10b634f3db..6aabdeb0fe 100644 --- a/accounts/abi/abigen/testdata/v2/tuple.go.txt +++ b/accounts/abi/abigen/testdata/v2/tuple.go.txt @@ -61,6 +61,11 @@ type Tuple struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Tuple) GetABI() abi.ABI { + return c.abi +} + // NewTuple creates a new instance of Tuple. func NewTuple() *Tuple { parsed, err := TupleMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/tupler.go.txt b/accounts/abi/abigen/testdata/v2/tupler.go.txt index 8643487042..5e6eafb07e 100644 --- a/accounts/abi/abigen/testdata/v2/tupler.go.txt +++ b/accounts/abi/abigen/testdata/v2/tupler.go.txt @@ -36,6 +36,11 @@ type Tupler struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Tupler) GetABI() abi.ABI { + return c.abi +} + // NewTupler creates a new instance of Tupler. func NewTupler() *Tupler { parsed, err := TuplerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/underscorer.go.txt b/accounts/abi/abigen/testdata/v2/underscorer.go.txt index 13ec968508..abe3bdbacb 100644 --- a/accounts/abi/abigen/testdata/v2/underscorer.go.txt +++ b/accounts/abi/abigen/testdata/v2/underscorer.go.txt @@ -36,6 +36,11 @@ type Underscorer struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Underscorer) GetABI() abi.ABI { + return c.abi +} + // NewUnderscorer creates a new instance of Underscorer. func NewUnderscorer() *Underscorer { parsed, err := UnderscorerMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/db/bindings.go b/accounts/abi/bind/v2/internal/contracts/db/bindings.go index 2fc57fba6d..b9a7e3164a 100644 --- a/accounts/abi/bind/v2/internal/contracts/db/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/db/bindings.go @@ -43,6 +43,11 @@ type DB struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DB) GetABI() abi.ABI { + return c.abi +} + // NewDB creates a new instance of DB. func NewDB() *DB { parsed, err := DBMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/events/bindings.go b/accounts/abi/bind/v2/internal/contracts/events/bindings.go index 2eb5751f23..7acd312b65 100644 --- a/accounts/abi/bind/v2/internal/contracts/events/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/events/bindings.go @@ -36,6 +36,11 @@ type C struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C) GetABI() abi.ABI { + return c.abi +} + // NewC creates a new instance of C. func NewC() *C { parsed, err := CMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go b/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go index d1cb08116b..406d71192f 100644 --- a/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go @@ -40,6 +40,11 @@ type C1 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C1) GetABI() abi.ABI { + return c.abi +} + // NewC1 creates a new instance of C1. func NewC1() *C1 { parsed, err := C1MetaData.ParseABI() @@ -118,6 +123,11 @@ type C2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C2) GetABI() abi.ABI { + return c.abi +} + // NewC2 creates a new instance of C2. func NewC2() *C2 { parsed, err := C2MetaData.ParseABI() @@ -192,6 +202,11 @@ type L1 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L1) GetABI() abi.ABI { + return c.abi +} + // NewL1 creates a new instance of L1. func NewL1() *L1 { parsed, err := L1MetaData.ParseABI() @@ -257,6 +272,11 @@ type L2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L2) GetABI() abi.ABI { + return c.abi +} + // NewL2 creates a new instance of L2. func NewL2() *L2 { parsed, err := L2MetaData.ParseABI() @@ -322,6 +342,11 @@ type L2b struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L2b) GetABI() abi.ABI { + return c.abi +} + // NewL2b creates a new instance of L2b. func NewL2b() *L2b { parsed, err := L2bMetaData.ParseABI() @@ -384,6 +409,11 @@ type L3 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L3) GetABI() abi.ABI { + return c.abi +} + // NewL3 creates a new instance of L3. func NewL3() *L3 { parsed, err := L3MetaData.ParseABI() @@ -450,6 +480,11 @@ type L4 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L4) GetABI() abi.ABI { + return c.abi +} + // NewL4 creates a new instance of L4. func NewL4() *L4 { parsed, err := L4MetaData.ParseABI() @@ -515,6 +550,11 @@ type L4b struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L4b) GetABI() abi.ABI { + return c.abi +} + // NewL4b creates a new instance of L4b. func NewL4b() *L4b { parsed, err := L4bMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go b/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go index 627b86f1b9..12d8b7f3e9 100644 --- a/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go @@ -36,6 +36,11 @@ type C struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C) GetABI() abi.ABI { + return c.abi +} + // NewC creates a new instance of C. func NewC() *C { parsed, err := CMetaData.ParseABI() @@ -173,6 +178,11 @@ type C2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C2) GetABI() abi.ABI { + return c.abi +} + // NewC2 creates a new instance of C2. func NewC2() *C2 { parsed, err := C2MetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go b/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go index 19d09bdd6a..f06788633c 100644 --- a/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go @@ -36,6 +36,11 @@ type MyContract struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *MyContract) GetABI() abi.ABI { + return c.abi +} + // NewMyContract creates a new instance of MyContract. func NewMyContract() *MyContract { parsed, err := MyContractMetaData.ParseABI() diff --git a/accounts/external/backend.go b/accounts/external/backend.go index 42eaf661cc..d73fb06ad0 100644 --- a/accounts/external/backend.go +++ b/accounts/external/backend.go @@ -237,6 +237,7 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio } if tx.Type() == types.BlobTxType { args.BlobHashes = tx.BlobHashes() + args.BlobFeeCap = (*hexutil.Big)(tx.BlobGasFeeCap()) sidecar := tx.BlobTxSidecar() if sidecar == nil { return nil, errors.New("blobs must be present for signing") diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 9b2ac14712..34712cb842 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -230,7 +230,7 @@ func toISO8601(t time.Time) string { if name == "UTC" { tz = "Z" } else { - tz = fmt.Sprintf("%03d00", offset/3600) + tz = fmt.Sprintf("%+03d00", offset/3600) } return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) diff --git a/accounts/manager.go b/accounts/manager.go index a2218e54dd..ebf7bbd62c 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -18,6 +18,7 @@ package accounts import ( "reflect" + "slices" "sort" "sync" @@ -254,13 +255,12 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { // drop is the counterpart of merge, which looks up wallets from within the sorted // cache and removes the ones specified. func drop(slice []Wallet, wallets ...Wallet) []Wallet { - for _, wallet := range wallets { - n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) - if n == len(slice) { - // Wallet not found, may happen during startup - continue - } - slice = append(slice[:n], slice[n+1:]...) + remove := make(map[URL]struct{}, len(wallets)) + for _, w := range wallets { + remove[w.URL()] = struct{}{} } - return slice + return slices.DeleteFunc(slice, func(w Wallet) bool { + _, ok := remove[w.URL()] + return ok + }) } diff --git a/accounts/manager_test.go b/accounts/manager_test.go new file mode 100644 index 0000000000..6727be156e --- /dev/null +++ b/accounts/manager_test.go @@ -0,0 +1,47 @@ +// 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 accounts + +import "testing" + +type managerTestWallet struct { + Wallet + url URL +} + +func (w managerTestWallet) URL() URL { + return w.url +} + +func TestDropMissingWallet(t *testing.T) { + t.Parallel() + + wallets := []Wallet{ + managerTestWallet{url: URL{Scheme: "test", Path: "a"}}, + managerTestWallet{url: URL{Scheme: "test", Path: "c"}}, + } + dropped := drop(wallets, managerTestWallet{url: URL{Scheme: "test", Path: "b"}}) + + if len(dropped) != len(wallets) { + t.Fatalf("drop removed wallet for missing URL: got %d wallets, want %d", len(dropped), len(wallets)) + } + for i := range dropped { + if got, want := dropped[i].URL(), wallets[i].URL(); got != want { + t.Fatalf("wallet %d mismatch: got %v, want %v", i, got, want) + } + } +} diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 84c7ef0c44..98b04311b4 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -126,7 +126,7 @@ var ( } extAddrFlag = &cli.StringFlag{ Name: "extaddr", - Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag.", + Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag. Provide a comma-separated pair to announce both an IPv4 and an IPv6 endpoint.", } crawlTimeoutFlag = &cli.DurationFlag{ Name: "timeout", @@ -344,36 +344,60 @@ func parseExtAddr(spec string) (ip net.IP, port int, ok bool) { func listen(ctx *cli.Context, ln *enode.LocalNode) *net.UDPConn { addr := ctx.String(listenAddrFlag.Name) + extAddr := ctx.String(extAddrFlag.Name) + var ( + socket net.PacketConn + err error + ) if addr == "" { - addr = "0.0.0.0:0" + // Dual-stack socket, falling back to IPv4-only where IPv6 is unavailable. + if socket, err = net.ListenPacket("udp", "[::]:0"); err != nil { + socket, err = net.ListenPacket("udp", "0.0.0.0:0") + } + } else { + socket, err = net.ListenPacket("udp", addr) } - socket, err := net.ListenPacket("udp4", addr) if err != nil { exit(err) } - // Configure UDP endpoint in ENR from listener address. + // Configure the ENR endpoint from the listener address, but only without an + // explicit -extaddr: otherwise we'd announce a fallback IP for an address + // family the user didn't specify (e.g. loopback IPv4 on an IPv6-only node). usocket := socket.(*net.UDPConn) uaddr := socket.LocalAddr().(*net.UDPAddr) - if uaddr.IP.IsUnspecified() { - ln.SetFallbackIP(net.IP{127, 0, 0, 1}) - } else { - ln.SetFallbackIP(uaddr.IP) + if extAddr == "" { + if uaddr.IP.IsUnspecified() { + ln.SetFallbackIP(net.IP{127, 0, 0, 1}) + } else { + ln.SetFallbackIP(uaddr.IP) + } } ln.SetFallbackUDP(uaddr.Port) - // If an ENR endpoint is set explicitly on the command-line, override - // the information from the listening address. Note this is careful not - // to set the UDP port if the external address doesn't have it. - extAddr := ctx.String(extAddrFlag.Name) + // Override with explicit -extaddr address(es). A static IP is set per family, + // and all specs share one UDP port because the node has a single socket. if extAddr != "" { - ip, port, ok := parseExtAddr(extAddr) - if !ok { - exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, extAddr)) + var extPort int + for spec := range strings.SplitSeq(extAddr, ",") { + spec = strings.TrimSpace(spec) + if spec == "" { + continue + } + ip, port, ok := parseExtAddr(spec) + if !ok { + exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, spec)) + } + ln.SetStaticIP(ip) + if port != 0 { + if extPort != 0 && port != extPort { + exit(fmt.Errorf("-%s: all addresses must announce the same UDP port, got %d and %d", extAddrFlag.Name, extPort, port)) + } + extPort = port + } } - ln.SetStaticIP(ip) - if port != 0 { - ln.SetFallbackUDP(port) + if extPort != 0 { + ln.SetFallbackUDP(extPort) } } @@ -406,6 +430,7 @@ type discv4API struct { func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) { it := api.host.RandomNodes() + defer it.Close() for len(ns) < n && it.Next() { ns = append(ns, it.Node()) } diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index dd253dd082..8b39829e60 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -58,6 +58,7 @@ var ( Action: discv5Crawl, Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{ crawlTimeoutFlag, + crawlParallelismFlag, }), } discv5TestCommand = &cli.Command{ diff --git a/cmd/devp2p/internal/ethtest/chain.go b/cmd/devp2p/internal/ethtest/chain.go index b44e6aa36a..36c395cdef 100644 --- a/cmd/devp2p/internal/ethtest/chain.go +++ b/cmd/devp2p/internal/ethtest/chain.go @@ -335,7 +335,7 @@ func readAccounts(file string) (map[common.Address]*senderInfo, error) { for addr, acc := range keys { pk, err := crypto.HexToECDSA(common.Bytes2Hex(acc.Key)) if err != nil { - return nil, fmt.Errorf("unable to read private key for %s: %v", err, addr) + return nil, fmt.Errorf("unable to read private key for %s: %v", addr, err) } accounts[addr] = &senderInfo{Key: pk, Nonce: 0} } diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index b486eb26d9..22247a5cf6 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -425,7 +425,7 @@ func (s *Suite) TestGetBlockBodies(t *utesting.T) { t.Fatalf("error reading block bodies msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetBlockBodiesRequest) { t.Fatalf("wrong bodies in response: expected %d bodies, got %d", len(req.GetBlockBodiesRequest), resp.List.Len()) @@ -469,7 +469,7 @@ func (s *Suite) TestGetReceipts(t *utesting.T) { t.Fatalf("error reading block receipts msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetReceiptsRequest) { t.Fatalf("wrong receipts in response: expected %d receipts, got %d", len(req.GetReceiptsRequest), resp.List.Len()) @@ -490,7 +490,7 @@ func (s *Suite) TestGetReceipts(t *utesting.T) { t.Fatalf("error reading block receipts msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetReceiptsRequest) { t.Fatalf("wrong receipts in response: expected %d receipts, got %d", len(req.GetReceiptsRequest), resp.List.Len()) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 4dc2507693..17d39d5ad0 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -186,7 +186,7 @@ func (s *Suite) TestHandshakeResend(t *utesting.T) { t.Fatalf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], challenge1.Nonce[:]) } if !bytes.Equal(resp.ChallengeData, challenge1.ChallengeData) { - t.Fatalf("wrong ChallengeData in resent WHOAREYOU (want %x)", resp.ChallengeData, challenge1.ChallengeData) + t.Fatalf("wrong ChallengeData in resent WHOAREYOU: got %x, want %x", resp.ChallengeData, challenge1.ChallengeData) } resp.Node = conn.remote default: diff --git a/cmd/evm/blockrunner.go b/cmd/evm/blockrunner.go index c6fac5396e..b175f05dc7 100644 --- a/cmd/evm/blockrunner.go +++ b/cmd/evm/blockrunner.go @@ -78,7 +78,7 @@ func blockTestCmd(ctx *cli.Context) error { return err } // During fuzzing, we report the result after every block - if !ctx.IsSet(FuzzFlag.Name) { + if !ctx.Bool(FuzzFlag.Name) { report(ctx, results) } } @@ -101,7 +101,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) { tracer := tracerFromFlags(ctx) // Suppress INFO logs during fuzzing - if ctx.IsSet(FuzzFlag.Name) { + if ctx.Bool(FuzzFlag.Name) { log.SetDefault(log.NewLogger(log.DiscardHandler())) } @@ -140,7 +140,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) { } // When fuzzing, write results after every block - if ctx.IsSet(FuzzFlag.Name) { + if ctx.Bool(FuzzFlag.Name) { report(ctx, []testResult{*result}) } results = append(results, *result) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index f35b28510c..d711ae56ac 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -242,6 +242,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, chainConfig.DAOForkBlock.Cmp(new(big.Int).SetUint64(pre.Env.Number)) == 0 { misc.ApplyDAOHardFork(statedb) } + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if pre.Env.Number > 0 && + chainConfig.IsAmsterdam(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) && + !chainConfig.IsAmsterdam(new(big.Int).SetUint64(pre.Env.Number-1), pre.Env.ParentTimestamp) { + misc.ApplyEIP7997(statedb) + } evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 9eb1bdbf5f..7207cad41d 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/tests" + "github.com/holiman/uint256" "github.com/urfave/cli/v2" ) @@ -132,8 +133,14 @@ func Transaction(ctx *cli.Context) error { r.Address = sender } // Check intrinsic gas + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + // A 256-bit overflow is reported by the field validation below; use a + // non-zero placeholder so intrinsic gas is still computed and reported. + value = uint256.NewInt(1) + } rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), value, rules, params.CostPerStateByte) if err != nil { r.Error = err results = append(results, r) @@ -147,7 +154,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(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, r.Address, tx.To(), value, tx.Data(), tx.AccessList()) if err != nil { r.Error = err results = append(results, r) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 891507d2ed..95b0ec274e 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -62,6 +62,7 @@ var ( Flags: slices.Concat([]cli.Flag{ utils.CachePreimagesFlag, utils.OverrideOsaka, + utils.OverrideAmsterdam, utils.OverrideBPO1, utils.OverrideBPO2, utils.OverrideUBT, @@ -287,6 +288,10 @@ func initGenesis(ctx *cli.Context) error { v := ctx.Uint64(utils.OverrideOsaka.Name) overrides.OverrideOsaka = &v } + if ctx.IsSet(utils.OverrideAmsterdam.Name) { + v := ctx.Uint64(utils.OverrideAmsterdam.Name) + overrides.OverrideAmsterdam = &v + } if ctx.IsSet(utils.OverrideBPO1.Name) { v := ctx.Uint64(utils.OverrideBPO1.Name) overrides.OverrideBPO1 = &v @@ -792,8 +797,8 @@ func downloadEra(ctx *cli.Context) error { var network = "mainnet" if utils.IsNetworkPreset(ctx) { switch { - case ctx.IsSet(utils.MainnetFlag.Name): - case ctx.IsSet(utils.SepoliaFlag.Name): + case ctx.Bool(utils.MainnetFlag.Name): + case ctx.Bool(utils.SepoliaFlag.Name): network = "sepolia" default: return errors.New("unsupported network, no known era1 checksums") @@ -820,7 +825,7 @@ func downloadEra(ctx *cli.Context) error { return err } switch { - case ctx.IsSet(eraAllFlag.Name): + case ctx.Bool(eraAllFlag.Name): return l.DownloadAll(dir) case ctx.IsSet(eraBlockFlag.Name): diff --git a/cmd/geth/config.go b/cmd/geth/config.go index ab9a336349..8ac9e7f6bd 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -228,6 +228,10 @@ func makeFullNode(ctx *cli.Context) *node.Node { v := ctx.Uint64(utils.OverrideOsaka.Name) cfg.Eth.OverrideOsaka = &v } + if ctx.IsSet(utils.OverrideAmsterdam.Name) { + v := ctx.Uint64(utils.OverrideAmsterdam.Name) + cfg.Eth.OverrideAmsterdam = &v + } if ctx.IsSet(utils.OverrideBPO1.Name) { v := ctx.Uint64(utils.OverrideBPO1.Name) cfg.Eth.OverrideBPO1 = &v diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d4acb6f652..abf2b2437c 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -57,6 +57,7 @@ var ( utils.USBFlag, utils.SmartCardDaemonPathFlag, utils.OverrideOsaka, + utils.OverrideAmsterdam, utils.OverrideBPO1, utils.OverrideBPO2, utils.OverrideUBT, @@ -177,6 +178,7 @@ var ( utils.AllowUnprotectedTxs, utils.BatchRequestLimit, utils.BatchResponseMaxSize, + utils.HTTPBodyLimitFlag, utils.RPCTxSyncDefaultTimeoutFlag, utils.RPCTxSyncMaxTimeoutFlag, utils.RPCGlobalRangeLimitFlag, @@ -290,13 +292,13 @@ func main() { func prepare(ctx *cli.Context) { // If we're running a known preset, log it for convenience. switch { - case ctx.IsSet(utils.SepoliaFlag.Name): + case ctx.Bool(utils.SepoliaFlag.Name): log.Info("Starting Geth on Sepolia testnet...") - case ctx.IsSet(utils.HoleskyFlag.Name): + case ctx.Bool(utils.HoleskyFlag.Name): log.Info("Starting Geth on Holesky testnet...") - case ctx.IsSet(utils.HoodiFlag.Name): + case ctx.Bool(utils.HoodiFlag.Name): log.Info("Starting Geth on Hoodi testnet...") case !ctx.IsSet(utils.NetworkIdFlag.Name): diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 8fedc21c27..c0258df68c 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -1054,6 +1054,7 @@ func listEIP7610EligibleAccounts(ctx *cli.Context) error { log.Error("Failed to get account iterator", "err", err) return err } + defer iter.Release() var ( start = time.Now() accounts []common.Address diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d2c6c217c7..7c363ce932 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -255,6 +255,11 @@ var ( Usage: "Manually specify the Osaka fork timestamp, overriding the bundled setting", Category: flags.EthCategory, } + OverrideAmsterdam = &cli.Uint64Flag{ + Name: "override.amsterdam", + Usage: "Manually specify the Amsterdam fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } OverrideBPO1 = &cli.Uint64Flag{ Name: "override.bpo1", Usage: "Manually specify the bpo1 fork timestamp, overriding the bundled setting", @@ -863,6 +868,12 @@ var ( Value: node.DefaultConfig.BatchResponseMaxSize, Category: flags.APICategory, } + HTTPBodyLimitFlag = &cli.IntFlag{ + Name: "rpc.http-body-limit", + Usage: "Maximum size (in megabytes) of an HTTP request body", + Value: node.DefaultConfig.HTTPBodyLimit / (1024 * 1024), + Category: flags.APICategory, + } // Network Settings MaxPeersFlag = &cli.IntFlag{ @@ -1363,6 +1374,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { if ctx.IsSet(BatchResponseMaxSize.Name) { cfg.BatchResponseMaxSize = ctx.Int(BatchResponseMaxSize.Name) } + + if ctx.IsSet(HTTPBodyLimitFlag.Name) { + cfg.HTTPBodyLimit = ctx.Int(HTTPBodyLimitFlag.Name) * 1024 * 1024 + } } // setGraphQL creates the GraphQL listener interface string from the set @@ -1910,7 +1925,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(RPCGlobalTxFeeCapFlag.Name) { cfg.RPCTxFeeCap = ctx.Float64(RPCGlobalTxFeeCapFlag.Name) } - if ctx.IsSet(NoDiscoverFlag.Name) { + if ctx.Bool(NoDiscoverFlag.Name) { cfg.EthDiscoveryURLs, cfg.SnapDiscoveryURLs = []string{}, []string{} } else if ctx.IsSet(DNSDiscoveryFlag.Name) { urls := ctx.String(DNSDiscoveryFlag.Name) @@ -2289,7 +2304,7 @@ func SplitTagsFlag(tagsFlag string) map[string]string { for _, t := range tags { if t != "" { - kv := strings.Split(t, "=") + kv := strings.SplitN(t, "=", 2) if len(kv) == 2 { tagsMap[kv[0]] = kv[1] @@ -2348,7 +2363,7 @@ func tryMakeReadOnlyDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database func IsNetworkPreset(ctx *cli.Context) bool { for _, flag := range NetworkFlags { bFlag, _ := flag.(*cli.BoolFlag) - if ctx.IsSet(bFlag.Name) { + if ctx.Bool(bFlag.Name) { return true } } diff --git a/cmd/utils/flags_test.go b/cmd/utils/flags_test.go index 0be3370d4a..81525ce3b2 100644 --- a/cmd/utils/flags_test.go +++ b/cmd/utils/flags_test.go @@ -18,8 +18,11 @@ package utils import ( + "flag" "reflect" "testing" + + "github.com/urfave/cli/v2" ) func Test_SplitTagsFlag(t *testing.T) { @@ -52,7 +55,9 @@ func Test_SplitTagsFlag(t *testing.T) { { "garbage", "smth=smthelse=123", - map[string]string{}, + map[string]string{ + "smth": "smthelse=123", + }, }, } for _, tt := range tests { @@ -64,3 +69,52 @@ func Test_SplitTagsFlag(t *testing.T) { }) } } + +func TestIsNetworkPresetUsesFlagValue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + want bool + }{ + { + name: "unset", + want: false, + }, + { + name: "enabled", + args: []string{"--sepolia"}, + want: true, + }, + { + name: "explicit false", + args: []string{"--sepolia=false"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := newTestContext(t, tt.args, NetworkFlags...) + if got := IsNetworkPreset(ctx); got != tt.want { + t.Fatalf("IsNetworkPreset() = %v, want %v", got, tt.want) + } + }) + } +} + +func newTestContext(t *testing.T, args []string, flags ...cli.Flag) *cli.Context { + t.Helper() + + set := flag.NewFlagSet("test", flag.ContinueOnError) + for _, f := range flags { + if err := f.Apply(set); err != nil { + t.Fatal(err) + } + } + if err := set.Parse(args); err != nil { + t.Fatal(err) + } + return cli.NewContext(nil, set, nil) +} diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go index 4e33522f1b..998dbfe165 100644 --- a/cmd/workload/testsuite.go +++ b/cmd/workload/testsuite.go @@ -122,7 +122,7 @@ func validateHistoryPruneErr(err error, blockNum uint64, historyPruneBlock *uint func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) { flags.CheckExclusive(ctx, testMainnetFlag, testSepoliaFlag) - if (ctx.IsSet(testMainnetFlag.Name) || ctx.IsSet(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) { + if (ctx.Bool(testMainnetFlag.Name) || ctx.Bool(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) { exit(filterQueryFileFlag.Name + " cannot be used with " + testMainnetFlag.Name + " or " + testSepoliaFlag.Name) } diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index 2ebf4f7155..add85092c2 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -73,8 +73,6 @@ func latestBlobConfig(cfg *params.ChainConfig, time uint64) (BlobConfig, error) bc = s.BPO2 case cfg.IsBPO1(london, time) && s.BPO1 != nil: bc = s.BPO1 - case cfg.IsOsaka(london, time) && s.Osaka != nil: - bc = s.Osaka case cfg.IsPrague(london, time) && s.Prague != nil: bc = s.Prague case cfg.IsCancun(london, time) && s.Cancun != nil: diff --git a/consensus/misc/eip4844/eip4844_test.go b/consensus/misc/eip4844/eip4844_test.go index 35934370af..4aa49c9cb1 100644 --- a/consensus/misc/eip4844/eip4844_test.go +++ b/consensus/misc/eip4844/eip4844_test.go @@ -120,7 +120,6 @@ func TestCalcBlobFeePostOsaka(t *testing.T) { BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: ¶ms.BlobConfig{ Target: 9, Max: 14, @@ -191,7 +190,7 @@ func TestFakeExponential(t *testing.T) { func TestCalcExcessBlobGasEIP7918(t *testing.T) { var ( cfg = params.MergedTestChainConfig - targetBlobs = cfg.BlobScheduleConfig.Osaka.Target + targetBlobs = cfg.BlobScheduleConfig.Prague.Target blobGasTarget = uint64(targetBlobs) * params.BlobTxBlobGasPerBlob ) diff --git a/consensus/misc/eip7997.go b/consensus/misc/eip7997.go new file mode 100644 index 0000000000..8564d5283a --- /dev/null +++ b/consensus/misc/eip7997.go @@ -0,0 +1,47 @@ +// 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 misc + +import ( + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// ApplyEIP7997 inserts the deterministic deployment factory into the state as an +// irregular state transition, as specified by EIP-7997. The factory is a keyless +// CREATE2 factory that, once present at the canonical address on every EVM chain, +// allows contracts to be deployed at identical addresses across chains. +func ApplyEIP7997(statedb vm.StateDB) { + // The account must hold the canonical factory runtime code. If its code hash + // already matches, the chain satisfies EIP-7997 and nothing needs to change. + wantHash := crypto.Keccak256Hash(params.DeterministicFactoryCode) + if statedb.GetCodeHash(params.DeterministicFactoryAddress) == wantHash { + return + } + if !statedb.Exist(params.DeterministicFactoryAddress) { + statedb.CreateAccount(params.DeterministicFactoryAddress) + } + statedb.CreateContract(params.DeterministicFactoryAddress) + statedb.SetCode(params.DeterministicFactoryAddress, params.DeterministicFactoryCode, tracing.CodeChangeUnspecified) + + // Preserve a pre-existing nonce; only bump the default zero nonce to 1. + if statedb.GetNonce(params.DeterministicFactoryAddress) == 0 { + statedb.SetNonce(params.DeterministicFactoryAddress, 1, tracing.NonceChangeNewContract) + } +} diff --git a/core/bal_test.go b/core/bal_test.go index 6b79f86c43..64c2714b16 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -42,9 +42,6 @@ import ( func balChainConfig() *params.ChainConfig { cfg := *params.MergedTestChainConfig cfg.AmsterdamTime = new(uint64) - blob := *cfg.BlobScheduleConfig - blob.Amsterdam = blob.Osaka - cfg.BlobScheduleConfig = &blob return &cfg } @@ -518,6 +515,73 @@ func TestBALStaticCallTargetIncluded(t *testing.T) { assertEmpty(t, assertPresent(t, b, target)) } +// makeValueCaller emits a single value-transferring CALL-family op (CALL 0xf1 +// or CALLCODE 0xf2) against `target` with value=1, then STOPs. Used together +// with a zero-balance caller to make the value transfer fail CanTransfer. +func makeValueCaller(op byte, target common.Address) []byte { + code := []byte{ + 0x60, 0x00, // retSize + 0x60, 0x00, // retOff + 0x60, 0x00, // argsSize + 0x60, 0x00, // argsOff + 0x60, 0x01, // value = 1 + 0x73, // PUSH20 target + } + code = append(code, target.Bytes()...) + return append(code, 0x5a, op, 0x50, 0x00) // GAS, op, POP, STOP +} + +// TestBALCallToDelegatedTargetBalanceFail asserts the EIP-7928 rule revised in +// ethereum/EIPs#11838: when a CALL targets an EIP-7702 delegated account and the +// delegated address passes its access_cost gas check, the delegated +// (implementation) address MUST appear in the BAL even when the call then fails +// its sender-balance check, because the delegation is resolved before that +// check. CALL routes through the EIP-8037 gas path. +func TestBALCallToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") // EOA carrying a 7702 designator + impl := common.HexToAddress("0x111111") // delegation target (implementation) + caller := common.HexToAddress("0xca11") // zero-balance contract issuing the CALL + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf1 /* CALL */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + // The call failed its sender-balance check, so the implementation never + // executed: it is recorded with an empty change set, but it MUST be present. + assertEmpty(t, assertPresent(t, b, impl)) +} + +// TestBALCallCodeToDelegatedTargetBalanceFail is the CALLCODE analogue of +// TestBALCallToDelegatedTargetBalanceFail, exercising the EIP-7702 gas path +// (CALLCODE/STATICCALL/DELEGATECALL) rather than the EIP-8037 one. +func TestBALCallCodeToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") + impl := common.HexToAddress("0x111111") + caller := common.HexToAddress("0xca11") + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf2 /* CALLCODE */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + assertEmpty(t, assertPresent(t, b, impl)) +} + // ============================== Revert behaviour ============================== // TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still @@ -946,6 +1010,62 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) { } } +// TestBALSelfDestructToSelfKeepsBalance: under EIP-8246 a freshly created +// contract that self-destructs to itself keeps its balance (it is not burnt and +// the account is not removed). The surviving balance-only account must therefore +// be recorded in the BAL with its preserved balance. +func TestBALSelfDestructToSelfKeepsBalance(t *testing.T) { + env := newBALTestEnv(nil) + // Init code: ADDRESS SELFDESTRUCT — the contract self-destructs to itself + // during its own creation transaction (satisfying EIP-6780's same-tx rule). + // ADDRESS (0x30) ; SELFDESTRUCT (0xff) + init := []byte{0x30, 0xff} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init)) + }) + + created := receipts[0].ContractAddress + cc := assertPresent(t, b, created) + // EIP-8246: balance preserved (not burnt), account survives -> the BAL must + // record the created address with its retained balance. + if len(cc.BalanceChanges) != 1 || cc.BalanceChanges[0].PostBalance.Uint64() != 100 { + t.Fatalf("self-destruct-to-self must preserve balance 100 in the BAL: %+v", cc.BalanceChanges) + } +} + +// TestBALSelfDestructToSelfPrefundedUnchanged: a pre-funded address onto which a +// contract is deployed and which self-destructs to itself in the same +// transaction. Under EIP-8246 the account survives with its balance unchanged, +// so the BAL must list it only as an access (no balance/nonce/code change). +func TestBALSelfDestructToSelfPrefundedUnchanged(t *testing.T) { + // The contract address created by the sender's nonce-0 transaction; it is + // pre-funded in genesis (balance only: nonce 0, no code, no storage), which + // EIP-7610 permits as a deployment target. + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + created := crypto.CreateAddress(crypto.PubkeyToAddress(key.PublicKey), 0) + + env := newBALTestEnv(types.GenesisAlloc{ + created: {Balance: big.NewInt(77)}, + }) + // Init code: ADDRESS SELFDESTRUCT, deployed with zero value so the balance is + // untouched (stays at the pre-funded 77). + init := []byte{0x30, 0xff} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) + }) + + if receipts[0].ContractAddress != created { + t.Fatalf("unexpected created address: have %x want %x", receipts[0].ContractAddress, created) + } + aa := assertPresent(t, b, created) + // EIP-8246: balance preserved and equal to the pre-transaction value, so no + // balance change; nonce and code end where they started (0 / empty). The + // account is only read, with an empty change set. + assertEmpty(t, aa) +} + // ============================== Mid-tx balance round-trip ============================== // TestBALMidTxBalanceRoundTrip: when an address's balance changes during a diff --git a/core/bench_test.go b/core/bench_test.go index fe66aeae0d..79584309da 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, params.Rules{}, params.CostPerStateByte) + cost, _ := IntrinsicGas(data, nil, nil, common.Address{}, &toaddr, nil, params.Rules{}, params.CostPerStateByte) 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 5d82fc1df8..9cbb489c4e 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -55,8 +55,9 @@ var ( UBTTime: u64(0), TerminalTotalDifficulty: common.Big0, EnableUBTAtGenesis: true, + // UBT inherits its blob schedule; nothing to declare here. BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - UBT: params.DefaultPragueBlobConfig, + Prague: params.DefaultPragueBlobConfig, }, } ) @@ -64,12 +65,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) // 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, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain diff --git a/core/chain_makers.go b/core/chain_makers.go index d93ce80dca..f02e0341bb 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -385,6 +385,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(b.header.Number) == 0 { misc.ApplyDAOHardFork(statedb) } + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if config.IsAmsterdam(b.header.Number, b.header.Time) && !config.IsAmsterdam(parent.Number(), parent.Time()) { + misc.ApplyEIP7997(statedb) + } if config.IsPrague(b.header.Number, b.header.Time) || config.IsUBT(b.header.Number, b.header.Time) { // EIP-2935 diff --git a/core/eip2780_test.go b/core/eip2780_test.go new file mode 100644 index 0000000000..d9545923ff --- /dev/null +++ b/core/eip2780_test.go @@ -0,0 +1,214 @@ +// 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 ( + "math/big" + "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" + "github.com/holiman/uint256" +) + +// TestEIP2780Intrinsic checks the intrinsic-gas decomposition. +func TestEIP2780Intrinsic(t *testing.T) { + var ( + from = common.HexToAddress("0x1111111111111111111111111111111111111111") + to = common.HexToAddress("0x2222222222222222222222222222222222222222") + ) + cases := []struct { + name string + to *common.Address + value *uint256.Int + want vm.GasCosts + }{ + { + name: "self-transfer", + to: &from, + value: uint256.NewInt(1), + want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + }, + { + name: "self-transfer/zero-value", + to: &from, + value: uint256.NewInt(0), + want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + }, + { + name: "zero-value call", + to: &to, + value: uint256.NewInt(0), + // TxBaseCost + ColdAccountAccess = 15,000 + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780}, + }, + { + name: "value transfer to existing EOA", + to: &to, + value: uint256.NewInt(1), + // TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000 + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + params.TxValueCost2780 + params.TransferLogCost2780}, + }, + { + name: "contract creation, value = 0", + to: nil, + value: uint256.NewInt(0), + // TxBaseCost + CreateAccess = 23,000 regular, plus one account creation in state. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "contract creation, value > 0", + to: nil, + value: uint256.NewInt(1), + // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular, plus account creation. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := IntrinsicGas(nil, nil, nil, from, tc.to, tc.value, rules8037, params.CostPerStateByte) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, tc.want) + } + }) + } +} + +// TestEIP2780Gas checks every "Transaction reference case" in +// the EIP-2780 specification end-to-end, asserting the two-dimensional charge +// (intrinsic + top-level + execution) recorded in the block gas pool. +func TestEIP2780Gas(t *testing.T) { + const ( + cold = params.ColdAccountAccess2780 + base = params.TxBaseCost2780 + valueCst = params.TxValueCost2780 + params.TransferLogCost2780 + ) + var ( + existingEOA = common.HexToAddress("0xe0a0000000000000000000000000000000000001") + stopContract = common.HexToAddress("0xc0de000000000000000000000000000000000001") + delegated = common.HexToAddress("0xde1e000000000000000000000000000000000001") + emptyTarget = common.HexToAddress("0x7a76000000000000000000000000000000000001") // never allocated + freshEOA = common.HexToAddress("0xbeef000000000000000000000000000000000001") // never allocated + ) + // Shared world: a funded EOA, a STOP contract and an account delegated to a + // non-existent (codeless) target. The delegation target is intentionally + // absent so resolving it executes no code. + base7702 := types.GenesisAlloc{ + existingEOA: {Balance: big.NewInt(1)}, + stopContract: {Code: []byte{0x00}}, // STOP + delegated: {Code: types.AddressToDelegation(emptyTarget)}, + } + // valueCreateTx builds a contract-creation transaction carrying value. + valueCreateTx := func(value int64) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, Nonce: 0, To: nil, Value: big.NewInt(value), + Gas: 300_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), + }) + } + + cases := []struct { + name string + tx *types.Transaction + wantRegular, wantState uint64 + }{ + // case 1: ETH transfer to self. + {"self-transfer", callTx(0, senderAddr, 1, 100_000, nil), base, 0}, + // case 2: no-transfer to an existing EOA. + {"zero-value/eoa", callTx(0, existingEOA, 0, 100_000, nil), base + cold, 0}, + // case 3: no-transfer to a contract. + {"zero-value/contract", callTx(0, stopContract, 0, 100_000, nil), base + cold, 0}, + // case 4: ETH transfer to an existing EOA. + {"value/eoa", callTx(0, existingEOA, 1, 100_000, nil), base + cold + valueCst, 0}, + // case 5: ETH transfer to a contract. + {"value/contract", callTx(0, stopContract, 1, 100_000, nil), base + cold + valueCst, 0}, + // case 6: no-transfer to a 7702-delegated account. + {"zero-value/delegated", callTx(0, delegated, 0, 100_000, nil), base + 2*cold, 0}, + // case 7: ETH transfer to a 7702-delegated account (no new-account charge). + {"value/delegated", callTx(0, delegated, 1, 100_000, nil), base + 2*cold + valueCst, 0}, + // case 8: ETH transfer creating a new account. + {"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState}, + // case 9: contract-creation transaction, value = 0. + {"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccess2780, newAccountState}, + // case 10: contract-creation transaction, value > 0. + {"create/value", valueCreateTx(1), base + params.CreateAccess2780 + params.TransferLogCost2780, newAccountState}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, gp, err := applyMsg(t, mkState(senderAlloc(base7702)), tc.tx) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if gp.cumulativeRegular != tc.wantRegular { + t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular) + } + if gp.cumulativeState != tc.wantState { + t.Errorf("state gas = %d, want %d", gp.cumulativeState, tc.wantState) + } + }) + } +} + +// TestEIP2780NewAccountFunded verifies that a value transfer creating a new +// account both materializes and funds the recipient. +func TestEIP2780NewAccountFunded(t *testing.T) { + fresh := common.HexToAddress("0xbeef000000000000000000000000000000000002") + sdb := mkState(senderAlloc(nil)) + if _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 300_000, nil)); err != nil { + t.Fatal(err) + } + if !sdb.Exist(fresh) || sdb.GetBalance(fresh).Cmp(uint256.NewInt(1)) != 0 { + t.Fatalf("recipient not funded: exist=%v balance=%v", sdb.Exist(fresh), sdb.GetBalance(fresh)) + } +} + +// TestEIP2780InsufficientGasForCallCharge verifies that a value transfer +// creating a new account, whose gas limit only covers the 21,000 intrinsic base +// and not the additional new-account state gas charged before the call executes, +// halts out of gas. The transaction stays valid (no consensus error) but +// execution fails and the recipient is not created. +func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { + fresh := common.HexToAddress("0xbeef000000000000000000000000000000000003") + sdb := mkState(senderAlloc(nil)) + res, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 21_000, nil)) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err != vm.ErrOutOfGas { + t.Fatalf("expected out of gas, got %v", res.Err) + } + if res.UsedGas != 21_000 { + t.Fatalf("expected used gas, got %v", res.UsedGas) + } + if sdb.Exist(fresh) { + t.Fatal("recipient should not be created when the call charge cannot be paid") + } +} diff --git a/core/eip7997_test.go b/core/eip7997_test.go new file mode 100644 index 0000000000..4c4d26507b --- /dev/null +++ b/core/eip7997_test.go @@ -0,0 +1,113 @@ +// 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 . + +// Tests for EIP-7997: the deterministic deployment factory inserted as an +// irregular state transition at the Amsterdam activation block. + +package core + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// TestApplyEIP7997 verifies the irregular state transition seeds the factory +// account with the canonical code and nonce. +func TestApplyEIP7997(t *testing.T) { + sdb := mkState(nil) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(got, params.DeterministicFactoryCode) { + t.Fatalf("factory code mismatch:\n got %x\nwant %x", got, params.DeterministicFactoryCode) + } + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 1 { + t.Fatalf("factory nonce = %d, want %d", got, 1) + } +} + +// TestApplyEIP7997Existing checks that a chain which already hosts the factory +// (for example via its keyless creation transaction) is left untouched, so the +// transition never rewrites an existing nonce. +func TestApplyEIP7997Existing(t *testing.T) { + sdb := mkState(types.GenesisAlloc{ + params.DeterministicFactoryAddress: {Code: params.DeterministicFactoryCode, Nonce: 5}, + }) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 5 { + t.Fatalf("existing factory nonce overwritten: got %d, want 5", got) + } +} + +// TestApplyEIP7997WrongCode checks that an account occupying the factory address +// with the wrong code is force-overwritten with the canonical runtime code, while +// a pre-existing non-zero nonce is preserved. +func TestApplyEIP7997WrongCode(t *testing.T) { + sdb := mkState(types.GenesisAlloc{ + params.DeterministicFactoryAddress: {Code: []byte{0x60, 0x00}, Nonce: 7}, + }) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(got, params.DeterministicFactoryCode) { + t.Fatalf("factory code not overwritten:\n got %x\nwant %x", got, params.DeterministicFactoryCode) + } + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 7 { + t.Fatalf("factory nonce = %d, want %d (existing nonce must be preserved)", got, 7) + } +} + +// TestEIP7997FactoryDeploys exercises the inserted factory bytecode: calling it +// with a salt followed by init code must CREATE2-deploy the contract at the +// canonical deterministic address and return that address (20 bytes, unpadded). +func TestEIP7997FactoryDeploys(t *testing.T) { + sdb := mkState(nil) + misc.ApplyEIP7997(sdb) + + var ( + caller = common.Address{0xca} + salt [32]byte + // initcode returning the single-byte runtime 0xfe: + // PUSH1 0xfe PUSH1 0x00 MSTORE8 PUSH1 0x01 PUSH1 0x00 RETURN + initcode = common.FromHex("60fe60005360016000f3") + ) + salt[31] = 0x42 + + input := append(append([]byte{}, salt[:]...), initcode...) + + ret, _, err := amsterdamCoreEVM(sdb).Call(caller, params.DeterministicFactoryAddress, input, vm.NewGasBudget(10_000_000, 0), new(uint256.Int)) + if err != nil { + t.Fatalf("factory call failed: %v", err) + } + + want := crypto.CreateAddress2(params.DeterministicFactoryAddress, salt, crypto.Keccak256(initcode)) + if len(ret) != 20 { + t.Fatalf("factory returned %d bytes, want 20", len(ret)) + } + if got := common.BytesToAddress(ret); got != want { + t.Fatalf("factory returned address %x, want %x", got, want) + } + if code := sdb.GetCode(want); !bytes.Equal(code, []byte{0xfe}) { + t.Fatalf("deployed runtime code = %x, want fe", code) + } +} diff --git a/core/eip8037_test.go b/core/eip8037_test.go index 8626c0b090..6d506cba96 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -191,7 +191,7 @@ var ( // A creation tx's intrinsic gas pre-charges one account creation as state gas. func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, nil, true, rules8037, params.CostPerStateByte) + cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } @@ -325,7 +325,7 @@ func clearSlots(addr common.Address, n int) (types.GenesisAlloc, []byte) { // tx_gas_used_before_refund (peak) exceeds the post-refund gas used. func TestGasUsedBeforeRefund(t *testing.T) { c := common.HexToAddress("0xc1ea0") - alloc, _ := clearSlots(c, 1) + alloc, _ := clearSlots(c, 4) res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil)) if err != nil { t.Fatal(err) @@ -351,8 +351,8 @@ func TestRefundCappedAt20Percent(t *testing.T) { // The EIP-7623 calldata floor is applied after the refund. func TestRefundCalldataFloorAfterRefund(t *testing.T) { data := make([]byte, 1000) // all-zero calldata: floor dominates a bare call - floor, _ := FloorDataGas(rules8037, data, nil) to := common.HexToAddress("0xeeee") + floor, _ := FloorDataGas(rules8037, senderAddr, &to, new(uint256.Int), data, nil) res, _, err := applyMsg(t, mkState(senderAlloc(nil)), callTx(0, to, 0, 1_000_000, data)) if err != nil { t.Fatal(err) @@ -367,7 +367,7 @@ func TestRefundFloorNegatesRefund(t *testing.T) { c := common.HexToAddress("0xc1ea1") alloc, _ := clearSlots(c, 1) data := make([]byte, 1000) - floor, _ := FloorDataGas(rules8037, data, nil) + floor, _ := FloorDataGas(rules8037, senderAddr, &c, new(uint256.Int), data, nil) res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, data)) if err != nil { t.Fatal(err) @@ -474,7 +474,7 @@ var delegate8037 = common.HexToAddress("0xde1e8a7e") // Intrinsic gas pre-charges the worst-case (account + indicator) per auth. func TestAuthIntrinsicWorstCase(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, false, rules8037, params.CostPerStateByte) + cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } diff --git a/core/eip8038_test.go b/core/eip8038_test.go new file mode 100644 index 0000000000..d8977c6dd2 --- /dev/null +++ b/core/eip8038_test.go @@ -0,0 +1,110 @@ +// 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 . + +// EIP-8038 authorization accounting tests. The per-authorization intrinsic gas +// pre-charges ACCOUNT_WRITE (regular) on top of REGULAR_PER_AUTH_BASE_COST. +// applyAuthorization refunds that ACCOUNT_WRITE to the refund counter in exactly +// the cases where no new account leaf is written: an invalid authorization, or +// an authority whose account already exists. These white-box tests invoke +// applyAuthorization directly and read the raw refund counter, so they observe +// the refund before the EIP-3529 cap is applied. + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// newAuthTestTransition builds a minimal stateTransition with a state reservoir, +// suitable for calling applyAuthorization directly. +func newAuthTestTransition(sdb *state.StateDB) *stateTransition { + st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000)) + st.gasRemaining = vm.NewGasBudget(0, 1_000_000) // reservoir for state-gas refills + return st +} + +// A net-new delegation on a fresh authority writes a new account leaf, so the +// intrinsic ACCOUNT_WRITE stands (no refund). +func TestAuthAccountWriteNetNewNoRefund(t *testing.T) { + auth, _ := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != 0 { + t.Fatalf("refund = %d, want 0 (net-new account write)", got) + } +} + +// A pre-existing authority writes no new account leaf, so the intrinsic +// ACCOUNT_WRITE is refunded. +func TestAuthAccountWriteExistsRefund(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam) + } +} + +// An invalid authorization is skipped without writing any account leaf, so its +// intrinsic ACCOUNT_WRITE is refunded. +func TestAuthAccountWriteInvalidRefund(t *testing.T) { + k, _ := crypto.HexToECDSA(authKeyA) + bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id + }) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil { + t.Fatal("expected invalid-authorization error") + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam) + } +} + +// The same authority across two authorizations writes its account leaf only +// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the +// account as existing) is refunded. +func TestAuthAccountWriteDuplicateOnce(t *testing.T) { + a0, _ := signAuth(t, authKeyA, delegate8037, 0) + a1, _ := signAuth(t, authKeyA, delegate8037, 1) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + delegates := map[common.Address]bool{} + if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != 0 { + t.Fatalf("refund after first auth = %d, want 0", got) + } + if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam) + } +} diff --git a/core/eip8246_test.go b/core/eip8246_test.go new file mode 100644 index 0000000000..18049d2279 --- /dev/null +++ b/core/eip8246_test.go @@ -0,0 +1,102 @@ +// 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 ( + "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/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// TestEIP8246SelfdestructNoBurn verifies that, once EIP-8246 is active +// (Amsterdam), a contract that is created and self-destructs to itself within +// the same transaction keeps its balance instead of burning it: the account +// survives as a balance-only account (no code, zero nonce, balance preserved). +// +// https://eips.ethereum.org/EIPS/eip-8246 +func TestEIP8246SelfdestructNoBurn(t *testing.T) { + var ( + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + value = big.NewInt(1_000_000) + // Init code: ADDRESS (0x30) ; SELFDESTRUCT (0xff). The created contract + // self-destructs to itself during its own creation transaction. + initcode = common.FromHex("30ff") + ) + // TODO: drop this hacky Amsterdam config initialization once the final + // Amsterdam config is available (mirrors TestEthTransferLogs). + config.AmsterdamTime = new(uint64) + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + addr1: {Balance: newGwei(1_000_000_000)}, + }, + } + // The contract created by addr1's first (nonce 0) transaction. + created := crypto.CreateAddress(addr1, 0) + + db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx := types.MustSignNewTx(key1, signer, &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + To: nil, // contract creation + Gas: 1_000_000, + GasFeeCap: newGwei(5), + GasTipCap: newGwei(5), + Value: value, + Data: initcode, + }) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(db, gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + // Read the post-state of the generated block directly. InsertChain is avoided + // on purpose: it would additionally verify the EIP-7928 block access list, + // which the chain-generation harness on this branch does not yet populate + // consistently — an orthogonal concern to the EIP-8246 state semantics under + // test here. + state, err := chain.StateAt(blocks[0].Header()) + if err != nil { + t.Fatalf("failed to obtain block state: %v", err) + } + // EIP-8246: the self-destructed, freshly-created contract keeps its balance + // rather than burning it, so the account survives. + if got := state.GetBalance(created); got.ToBig().Cmp(value) != 0 { + t.Errorf("created account balance = %v, want %v (EIP-8246: balance must be preserved, not burned)", got, value) + } + // It survives as a balance-only account: nonce reset to 0 and no code. + if got := state.GetNonce(created); got != 0 { + t.Errorf("created account nonce = %d, want 0", got) + } + if got := state.GetCodeSize(created); got != 0 { + t.Errorf("created account code size = %d, want 0 (code must be cleared)", got) + } +} diff --git a/core/error.go b/core/error.go index 7dd5b8a432..26b007f9d9 100644 --- a/core/error.go +++ b/core/error.go @@ -66,11 +66,6 @@ var ( // have enough funds for transfer(topmost call only). ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") - // ErrInsufficientBalanceWitness is returned if the transaction sender has enough - // funds to cover the transfer, but not enough to pay for witness access/modification - // costs for the transaction - ErrInsufficientBalanceWitness = errors.New("insufficient funds to cover witness access costs for transaction") - // ErrInsufficientFunds is returned if the total cost of executing a transaction // is higher than the balance of the user's account. ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value") diff --git a/core/eth_transfer_logs_test.go b/core/eth_transfer_logs_test.go index 815b56b588..a5a5f1894a 100644 --- a/core/eth_transfer_logs_test.go +++ b/core/eth_transfer_logs_test.go @@ -78,11 +78,7 @@ func testEthTransferLogs(t *testing.T, value uint64) { engine = beacon.New(ethash.NewFaker()) ) - //TODO remove this hacky config initialization when final Amsterdam config is available config.AmsterdamTime = new(uint64) - blobConfig := *config.BlobScheduleConfig - blobConfig.Amsterdam = blobConfig.Osaka - config.BlobScheduleConfig = &blobConfig gspec := &Genesis{ Config: &config, diff --git a/core/genesis.go b/core/genesis.go index ec3e70a975..7fad915ce2 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -277,10 +277,11 @@ func (e *GenesisMismatchError) Error() string { // ChainOverrides contains the changes to chain config. type ChainOverrides struct { - OverrideOsaka *uint64 - OverrideBPO1 *uint64 - OverrideBPO2 *uint64 - OverrideUBT *uint64 + OverrideOsaka *uint64 + OverrideAmsterdam *uint64 + OverrideBPO1 *uint64 + OverrideBPO2 *uint64 + OverrideUBT *uint64 } // apply applies the chain overrides on the supplied chain config. @@ -291,6 +292,9 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error { if o.OverrideOsaka != nil { cfg.OsakaTime = o.OverrideOsaka } + if o.OverrideAmsterdam != nil { + cfg.AmsterdamTime = o.OverrideAmsterdam + } if o.OverrideBPO1 != nil { cfg.BPO1Time = o.OverrideBPO1 } @@ -721,6 +725,8 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis { // EIP-8282 - Builder Execution Requests params.BuilderDepositAddress: {Nonce: 1, Code: params.BuilderDepositCode, Balance: common.Big0}, params.BuilderExitAddress: {Nonce: 1, Code: params.BuilderExitCode, Balance: common.Big0}, + // EIP-7997 - Deterministic deployment factory + params.DeterministicFactoryAddress: {Nonce: 1, Code: params.DeterministicFactoryCode, Balance: common.Big0}, }, } if faucet != nil { diff --git a/core/genesis_test.go b/core/genesis_test.go index 67b88fc415..80ee53493e 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -293,8 +293,6 @@ func TestBinaryGenesisCommit(t *testing.T) { BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, - UBT: params.DefaultPragueBlobConfig, }, } diff --git a/core/rawdb/freezer_meta.go b/core/rawdb/freezer_meta.go index 3cda9ae45c..03c26f7daf 100644 --- a/core/rawdb/freezer_meta.go +++ b/core/rawdb/freezer_meta.go @@ -112,7 +112,7 @@ func decodeV2(file *os.File) *freezerTableMeta { return nil } if o.Offset > math.MaxInt64 { - log.Error("Invalid flushOffset %d in freezer metadata", o.Offset, "file", file.Name()) + log.Error("Invalid flushOffset %d in freezer metadata", "offset", o.Offset, "file", file.Name()) return nil } return &freezerTableMeta{ diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index c770e89989..d18b75c2f7 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -611,9 +611,18 @@ func (t *freezerTable) truncateHead(items uint64) error { if existing <= items { return nil } - if items < t.itemHidden.Load() { + + hidden := t.itemHidden.Load() + + if items < hidden { + if existing == hidden { + // Empty table means that it is newly added. Its tail would be + // at the head, so we have to align the table down to the new head. + return t.resetTo(items) + } return errors.New("truncation below tail") } + // We need to truncate, save the old size for metrics tracking oldSize, err := t.sizeNolock() if err != nil { diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index 0fc4f90011..2197a29772 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -516,7 +516,7 @@ func TestFreezerCloseSync(t *testing.T) { if err := f.SyncAncient(); err == nil { t.Fatalf("want error, have nil") } else if have, want := err.Error(), "[closed closed]"; have != want { - t.Fatalf("want %v, have %v", have, want) + t.Fatalf("want %v, have %v", want, have) } } diff --git a/core/state/database.go b/core/state/database.go index 3b1e627f28..1001a4b4cf 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -107,12 +107,18 @@ type Trie interface { // in the trie with provided address. UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error + // UpdateAccountBatch attempts to update a list of accounts in the batch manner. + UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLengths []int) error + // UpdateStorage associates key with value in the trie. If value has length zero, // any existing value is deleted from the trie. The value bytes must not be modified // by the caller while they are stored in the trie. If a node was not found in the // database, a trie.MissingNodeError is returned. UpdateStorage(addr common.Address, key, value []byte) error + // UpdateStorageBatch attempts to update a list of storages in the batch manner. + UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error + // DeleteAccount abstracts an account deletion from the trie. DeleteAccount(address common.Address) error diff --git a/core/state/statedb.go b/core/state/statedb.go index 1c49d46020..09a896a89a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -614,16 +614,17 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { } s.AccountReads += time.Since(start) - // Short circuit if the account is not found - if acct == nil { - return nil - } - // Schedule the resolved account for prefetching if it's enabled. + // Schedule the account path for prefetching if it's enabled. Even if the + // account is absent, the trie path proves its non-existence for witnesses. if s.prefetcher != nil { if err = s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, []common.Address{addr}, nil, true); err != nil { log.Error("Failed to prefetch account", "addr", addr, "err", err) } } + // Short circuit if the account is not found + if acct == nil { + return nil + } // Insert into the live set obj := newObject(s, addr, acct) s.setStateObject(obj) @@ -764,50 +765,15 @@ func (s *StateDB) GetRefund() uint64 { return s.refund } -type removedAccountWithBalance struct { - address common.Address - balance *uint256.Int -} - -// LogsForBurnAccounts returns the eth burn logs for accounts scheduled for -// removal which still have positive balance. The purpose of this function is -// to handle a corner case of EIP-7708 where a self-destructed account might -// still receive funds between sending/burning its previous balance and actual -// removal. In this case the burning of these remaining balances still need to -// be logged. -// Specification EIP-7708: https://eips.ethereum.org/EIPS/eip-7708 -// -// This function should only be invoked at the transaction boundary, specifically -// before the Finalise. -func (s *StateDB) LogsForBurnAccounts() []*types.Log { - var list []removedAccountWithBalance - 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, - balance: obj.Balance(), - }) - } - } - if list == nil { - return nil - } - sort.Slice(list, func(i, j int) bool { - return list[i].address.Cmp(list[j].address) < 0 - }) - logs := make([]*types.Log, len(list)) - for i, acct := range list { - logs[i] = types.EthBurnLog(acct.address, acct.balance) - } - return logs -} - // 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.ConstructionBlockAccessList { + if s.stateAccessList != nil { + return s.finaliseAmsterdam(deleteEmptyObjects) + } addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations)) - for addr, state := range s.journal.mutations { + for addr := range s.journal.mutations { obj, exist := s.stateObjects[addr] if !exist { // RIPEMD160 (0x03) gets an extra dirty marker for a historical @@ -831,46 +797,103 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccess 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) } + addressesToPrefetch = append(addressesToPrefetch, addr) // Copy needed for closure + } + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + if err := s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch, nil, false); err != nil { + log.Error("Failed to prefetch addresses", "addresses", len(addressesToPrefetch), "err", err) + } + } + // Invalidate journal because reverting across transactions is not allowed. + s.clearJournalAndRefund() + + return nil +} + +func (s *StateDB) recordAccessListChanges(addr common.Address, state *journalMutationState) { + var ( + balance = uint256.NewInt(0) + nonce uint64 + ) + obj := s.stateObjects[addr] // nil when the account was removed + if obj != nil { + balance, nonce = obj.Balance(), obj.Nonce() + } + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + if state.nonceSet && nonce != state.nonce { + s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce) + } + if state.codeSet { + var code []byte + if obj != nil { + code = obj.Code() + } + if !bytes.Equal(code, state.code) { + s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code) + } + } +} + +// finaliseAmsterdam is the Amsterdam-and-later variant of Finalise. +func (s *StateDB) finaliseAmsterdam(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 { + // 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 + } + switch { + case obj.selfDestructed: + // EIP-8264: accounts marked for self-destruction, instead of + // being deleted, are modified as follows: + // - nonce is reset to 0, + // - balance is unchanged, + // - code is cleared, + // - all storage is cleared + if !obj.Balance().IsZero() { + o := newObject(s, obj.address, obj.origin) + o.setBalance(new(uint256.Int).Set(obj.Balance())) + s.setStateObject(o) + s.markUpdate(addr) + } else { + delete(s.stateObjects, obj.address) + s.markDelete(addr) + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + s.stateObjectsDestruct[obj.address] = obj + } + } + + case deleteEmptyObjects && obj.empty(): + // EIP-161: a touched, empty account is removed. + delete(s.stateObjects, obj.address) + s.markDelete(addr) + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + s.stateObjectsDestruct[obj.address] = obj + } + + default: + obj.finalise() + s.markUpdate(addr) + } + // Aggregate the resulting account metadata change + // into the block-level access list. + s.recordAccessListChanges(addr, state) + // At this point, also ship the address off to the precacher. The precacher // will start loading tries, and when the change is eventually committed, // the commit-phase will be a lot faster diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 98d01343a4..73e8253311 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -230,10 +230,6 @@ func (s *hookedStateDB) AddLog(log *types.Log) { } } -func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { - return s.inner.LogsForBurnAccounts() -} - 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. @@ -256,18 +252,24 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlock return bytes.Compare(selfDestructedAddrs[i][:], selfDestructedAddrs[j][:]) < 0 }) + // EIP-8246 (Amsterdam) removes the SELFDESTRUCT burn: a self-destructed + // account that retains a non-zero balance is preserved as a balance-only + // account rather than removed, so its balance is no longer burnt. + burnsBalance := s.inner.stateAccessList == nil + for _, addr := range selfDestructedAddrs { obj := s.inner.stateObjects[addr] // Bingo: state object was self-destructed, call relevant hooks. - // If ether was sent to account post-selfdestruct, record as burnt. - if s.hooks.OnBalanceChange != nil { + if burnsBalance && s.hooks.OnBalanceChange != nil { if bal := obj.Balance(); bal.Sign() != 0 { s.hooks.OnBalanceChange(addr, bal.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestructBurn) } } // Nonce is set to reset on self-destruct. + // + // TODO(rjl) shall we emit the nonce change if the pre-tx nonce was zero? if s.hooks.OnNonceChangeV2 != nil { s.hooks.OnNonceChangeV2(addr, obj.Nonce(), 0, tracing.NonceChangeSelfdestruct) } else if s.hooks.OnNonceChange != nil { diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 0bf9b50e7b..6f4282054e 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -998,6 +999,45 @@ func TestDeleteCreateRevert(t *testing.T) { } } +func TestWitnessIncludesAbsentAccountReads(t *testing.T) { + db := NewDatabaseForTesting() + state, _ := New(types.EmptyRootHash, db) + for i := byte(0); i < 3; i++ { + addr := common.Address{i + 1} + state.SetBalance(addr, uint256.NewInt(uint64(i+1)), tracing.BalanceChangeUnspecified) + } + root, err := state.Commit(0, false, false) + if err != nil { + t.Fatalf("failed to commit initial state: %v", err) + } + state, err = New(root, db) + if err != nil { + t.Fatalf("failed to reopen state: %v", err) + } + + witness := &stateless.Witness{ + Codes: make(map[string]struct{}), + State: make(map[string]struct{}), + } + state.StartPrefetcher("test", witness) + missing := common.HexToAddress("0x017655eac00c837122cabbbc0dd604a196906648") + if balance := state.GetBalance(missing); balance.Sign() != 0 { + t.Fatalf("unexpected balance for absent account: %v", balance) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after read: %v", err) + } + if got := state.IntermediateRoot(false); got != root { + t.Fatalf("unexpected root after read-only access: have %x want %x", got, root) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after root calculation: %v", err) + } + if len(witness.State) == 0 { + t.Fatal("missing witness nodes for absent account read") + } +} + // TestMissingTrieNodes tests that if the StateDB fails to load parts of the trie, // the Commit operation fails with an error // If we are missing trie nodes, we should not continue writing to the trie diff --git a/core/state_processor.go b/core/state_processor.go index 48a8da4062..27009cb49a 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -81,6 +81,10 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(tracingStateDB) } + parent := p.chain.GetHeader(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, fmt.Errorf("missing parent %#x", block.ParentHash()) + } var ( context = NewEVMBlockContext(header, p.chain, nil) signer = types.MakeSigner(config, header.Number, header.Time) @@ -88,12 +92,12 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated blockAccessList = bal.NewConstructionBlockAccessList() ) defer evm.Release() + if jumpDestCache != nil { evm.SetJumpDestCache(jumpDestCache) } - // Run the pre-execution system calls - blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) + blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), parent, config, evm, block.Number(), block.Time())) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { @@ -137,14 +141,20 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated }, nil } -// 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 { +// PreExecution processes pre-execution state changes and system calls. +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent *types.Header, 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-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if !config.IsAmsterdam(parent.Number, parent.Time) { + misc.ApplyEIP7997(evm.StateDB) + } } // EIP-4788 if beaconRoot != nil { @@ -152,7 +162,7 @@ func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Ha } // EIP-2935 if config.IsPrague(number, time) || config.IsUBT(number, time) { - ProcessParentBlockHash(parent, evm, blockAccessList) + ProcessParentBlockHash(parent.Hash(), evm, blockAccessList) } return blockAccessList } diff --git a/core/state_transition.go b/core/state_transition.go index bbeb163b16..a3a9efeaa4 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,23 +68,26 @@ 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 bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, from common.Address, to *common.Address, value *uint256.Int, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { + isContractCreation := to == nil + // Set the starting gas for the raw transaction var gas vm.GasCosts - if isContractCreation && rules.IsHomestead { - if rules.IsAmsterdam { - gas.RegularGas = params.TxGas + params.CreateGasAmsterdam + if rules.IsAmsterdam { + gas.RegularGas = intrinsicBaseGasEIP2780(from, to, value) + if isContractCreation { + // New-account creation is charged as state gas (EIP-8037). gas.StateGas = params.AccountCreationSize * costPerStateByte - } else { - gas.RegularGas = params.TxGasContractCreation } + } else if isContractCreation && rules.IsHomestead { + gas.RegularGas = params.TxGasContractCreation } else { gas.RegularGas = params.TxGas } // Add gas for authorizations if authList != nil { if rules.IsAmsterdam { - gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas + gas.RegularGas += uint64(len(authList)) * (params.AccountWriteAmsterdam + params.RegularPerAuthBaseCost) gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte } else { gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas @@ -123,14 +126,22 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { + + // Amsterdam re-prices the per-entry access-list cost + addressCost := params.TxAccessListAddressGas + storageKeyCost := params.TxAccessListStorageKeyGas + if rules.IsAmsterdam { + addressCost = params.TxAccessListAddressGasAmsterdam + storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam + } + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * storageKeyCost // EIP-7981: access list data is charged in addition to the base charge. if rules.IsAmsterdam { @@ -151,8 +162,41 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } +// intrinsicBaseGasEIP2780 computes the regular-gas portion of the EIP-2780 +// intrinsic base cost: the per-resource decomposition of the legacy flat 21,000. +func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int) uint64 { + var ( + isContractCreation = to == nil + isSelfTransfer = to != nil && *to == from + hasValue = value != nil && !value.IsZero() + ) + // tx.sender: signature recovery plus the sender account access and write. + gas := params.TxBaseCost2780 + + // tx.to charge. + switch { + case isSelfTransfer: + // The recipient account is already accessed and written as the sender. + case isContractCreation: + gas += params.CreateAccess2780 + default: + gas += params.ColdAccountAccess2780 + } + + // tx.value charge. + switch { + case !hasValue || isSelfTransfer: + // No transfer log and no recipient balance write. + case isContractCreation: + gas += params.TransferLogCost2780 + default: + gas += params.TransferLogCost2780 + params.TxValueCost2780 + } + return gas +} + // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). -func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { +func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, value *uint256.Int, data []byte, accessList types.AccessList) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -198,12 +242,17 @@ func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) tokenCost = params.TxCostFloorPerToken } + // The floor is anchored to the transaction base cost. + floorBase := params.TxGas + if rules.IsAmsterdam { + floorBase = params.TxBaseCost2780 + } // Check for overflow - if (math.MaxUint64-params.TxGas)/tokenCost < tokens { + if (math.MaxUint64-floorBase)/tokenCost < tokens { return 0, ErrGasUintOverflow } // Minimum gas required for a transaction based on its data tokens (EIP-7623). - return params.TxGas + tokens*tokenCost, nil + return floorBase + tokens*tokenCost, nil } // toWordSize returns the ceiled word size required for init code payment calculation. @@ -614,7 +663,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { contractCreation = msg.To == nil floorDataGas uint64 ) - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } @@ -629,7 +678,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Validate the EIP-7623 calldata floor against the gas limit. The floor inflates // the total gas usage at tx end, so the gas limit must be sufficient to cover that. if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) + floorDataGas, err = FloorDataGas(rules, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList) if err != nil { return nil, err } @@ -688,7 +737,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // If the contract creation failed, or the destination was pre-existing, // refund the account-creation state gas pre-charged in IntrinsicGas. if rules.IsAmsterdam && !creation { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } } else { // Increment the nonce for the next transaction. @@ -704,10 +753,21 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // performing the resolution and warming. if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) + // Record in BAL + if rules.IsAmsterdam { + st.state.GetCode(addr) + } + } + // EIP-2780: charge the transaction's top-level recipient costs. If the + // budget cannot cover the charge, the top frame halts out of gas. + if rules.IsAmsterdam && !st.chargeCallRecipientEIP2780(value) { + vmerr = vm.ErrOutOfGas + st.gasRemaining = st.gasRemaining.ExitHalt() + } else { + // Execute the transaction's call. + ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) } - // Execute the transaction's call. - ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) } // Settle down the gas usage and refund the ETH back if any remaining @@ -740,12 +800,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } - // EIP-7708: Emit the ETH-burn logs - if rules.IsAmsterdam { - for _, log := range st.evm.StateDB.LogsForBurnAccounts() { - st.evm.StateDB.AddLog(log) - } - } return &ExecutionResult{ UsedGas: gasUsed, MaxUsedGas: peakUsed, @@ -754,6 +808,47 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { }, nil } +// chargeCallRecipientEIP2780 applies the EIP-2780 transaction top-level gas costs for +// a message-call transaction, charged before any opcode executes: +// +// - if the recipient is EIP-161 non-existent and the transaction carries value, +// charge for account creation. +// +// - if the recipient is an EIP-7702 delegated account, resolving the delegation +// loads the target's code, charged an additional cold account access in +// regular gas. +func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { + var ( + cost vm.GasCosts + to = *st.msg.To + ) + // This runs in the topmost frame before any bytecode executes, so unlike the + // execution-level checks which must use StateDB.Empty because SELFDESTRUCT can + // leave a transient EIP-161-empty account, no empty account can exist here, and + // !Exist is equivalent to Empty. + if !value.IsZero() && !st.state.Exist(to) { + cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte + } + if _, ok := types.ParseDelegation(st.state.GetCode(to)); ok { + // EIP-2780: The tx.sender, tx.to, and (where applicable) delegation-target + // charges above are always at the cold rate. + // + // The delegation-target is already warmed before, no double warming here. + cost.RegularGas += params.ColdAccountAccess2780 + } + if cost == (vm.GasCosts{}) { + return true + } + prior, ok := st.gasRemaining.Charge(cost) + if !ok { + return false + } + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) + } + return true +} + // settleGas finalizes the per-tx gas accounting after EVM execution: // // - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas, @@ -870,20 +965,13 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio } // applyAuthorization applies an EIP-7702 code delegation to the state and, -// under EIP-8037, reconciles the per-authorization intrinsic state-gas pre- -// charge so that, per authority: -// -// - the account portion (AccountCreationSize × CPSB) is charged at most -// once, and only when the account did not exist before the tx -// -// - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is -// charged at most once, and only when the authority ends the tx delegated -// having started it undelegated. +// adjust the pre-charged intrinsic cost accordingly. func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { authority, err := st.validateAuthorization(auth) if err != nil { if rules.IsAmsterdam { - st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.state.AddRefund(params.AccountWriteAmsterdam) } return err } @@ -895,7 +983,8 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } } else { if st.state.Exist(authority) { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.state.AddRefund(params.AccountWriteAmsterdam) } authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte @@ -906,17 +995,17 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } if auth.Address == (common.Address{}) { // Clearing writes no indicator, refill this auth's state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) // The indicator was created by an earlier auth within the same // transaction, refill the state charge as it's no longer justified. if curDelegated && !preDelegated { - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } else if curDelegated || preDelegated { // The 23-byte slot is already occupied, overwriting it writes no // new bytes, refill the state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } diff --git a/core/state_transition_test.go b/core/state_transition_test.go index be2de7f511..60edad52e5 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) func TestFloorDataGas(t *testing.T) { @@ -73,21 +74,22 @@ func TestFloorDataGas(t *testing.T) { { name: "amsterdam/empty", amsterdam: true, - want: params.TxGas, + // EIP-2780 anchors the floor to the reduced base cost. + want: params.TxBaseCost2780, }, { 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, + want: params.TxBaseCost2780 + 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, + want: params.TxBaseCost2780 + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/access-list-addresses-only", @@ -97,7 +99,7 @@ func TestFloorDataGas(t *testing.T) { {Address: addr2}, }, // 2 * 20 bytes * 4 tokens/byte * 16 cost/token - want: params.TxGas + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/access-list-with-storage-keys", @@ -106,7 +108,7 @@ func TestFloorDataGas(t *testing.T) { {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, + want: params.TxBaseCost2780 + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/mixed", @@ -117,13 +119,13 @@ func TestFloorDataGas(t *testing.T) { {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, + want: params.TxBaseCost2780 + (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) + got, err := FloorDataGas(rules, addr1, &addr1, new(uint256.Int), tt.data, tt.accessList) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -155,6 +157,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028 bool isEIP3860 bool isAmsterdam bool + value *uint256.Int want vm.GasCosts }{ { @@ -237,9 +240,10 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // base access-list charge + EIP-7981 extra - want: vm.GasCosts{RegularGas: params.TxGas + - 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + + // EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess + // (15,000). Plus base access-list charge + EIP-7981 extra. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + 2*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, { @@ -259,10 +263,10 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isAmsterdam: true, - // EIP-8037: creation regular gas is TxGas + CreateGasAmsterdam (not TxGasContractCreation), - // and account-creation cost is moved to state gas. + // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000), + // and account-creation cost is charged as state gas. want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam, + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, }, @@ -275,7 +279,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, // Shanghai gates init-code word gas isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam + + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, @@ -292,9 +296,9 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam + + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, @@ -310,18 +314,41 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // EIP-8037 splits the auth-tuple charge into regular + state gas: - // regular: TxAuthTupleRegularGas (7500) per auth + // EIP-8037 splits the auth-tuple charge into regular + state gas, with + // the values finalized by EIP-8038: + // regular: ACCOUNT_WRITE (8,000) + REGULAR_PER_AUTH_BASE_COST (7,500) per auth // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth want: vm.GasCosts{ - RegularGas: params.TxGas + + RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + 100*params.TxDataNonZeroGasEIP2028 + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*params.TxAuthTupleRegularGas, + 1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost), StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, }, }, + { + name: "amsterdam/value-transfer-call", + isEIP2028: true, + isAmsterdam: true, + value: uint256.NewInt(1), + // EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + params.TransferLogCost2780 + params.TxValueCost2780}, + }, + { + name: "amsterdam/value-bearing-contract-creation", + creation: true, + isHomestead: true, + isEIP2028: true, + isAmsterdam: true, + value: uint256.NewInt(1), + // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756, plus account-creation state gas. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -331,8 +358,12 @@ func TestIntrinsicGas(t *testing.T) { IsShanghai: tt.isEIP3860, IsAmsterdam: tt.isAmsterdam, } + var to *common.Address + if !tt.creation { + to = &addr1 + } got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, - tt.creation, rules, params.CostPerStateByte) + common.Address{}, to, tt.value, rules, params.CostPerStateByte) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 4e5f4f5e5a..56e018d175 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1235,11 +1235,6 @@ func TestBillyMigration(t *testing.T) { Max: maxBlobs, UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, }, - Osaka: ¶ms.BlobConfig{ - Target: maxBlobs / 2, - Max: maxBlobs, - UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, - }, }, } chain := &testBlockChain{ diff --git a/core/txpool/blobpool/cache_test.go b/core/txpool/blobpool/cache_test.go index a410208bcc..1902f9b353 100644 --- a/core/txpool/blobpool/cache_test.go +++ b/core/txpool/blobpool/cache_test.go @@ -94,7 +94,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache { CancunTime: &cancunTime, OsakaTime: &cancunTime, BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - Osaka: ¶ms.BlobConfig{ + Cancun: ¶ms.BlobConfig{ Target: 1, Max: 1, UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index 8e44f51fdd..e599877f2a 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -107,7 +107,8 @@ func (l *limbo) Close() error { func (l *limbo) parseBlob(id uint64, data []byte) error { item := new(limboBlob) if err := rlp.DecodeBytes(data, item); err != nil { - if isLegacyLimboBlob(data) { + // This entry may have been stored with the legacy limboBlob type + if isLegacy(data) { return errLegacyTx } // This path is impossible unless the disk data representation changes @@ -133,9 +134,9 @@ func (l *limbo) parseBlob(id uint64, data []byte) error { return nil } -// isLegacyLimboBlob returns true if the data is encoded in legacy limboBlob type. -// It checks whether the first byte of third element is blobTxType. -func isLegacyLimboBlob(data []byte) bool { +// isLegacy reports whether data is a limbo entry with the legacy limboBlob +// {TxHash, Block, Tx *types.Transaction} type. +func isLegacy(data []byte) bool { elems, err := rlp.SplitListValues(data) if err != nil || len(elems) < 3 { return false diff --git a/core/txpool/blobpool/limbo_test.go b/core/txpool/blobpool/limbo_test.go new file mode 100644 index 0000000000..04cb5f5c20 --- /dev/null +++ b/core/txpool/blobpool/limbo_test.go @@ -0,0 +1,69 @@ +// Copyright 2025 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 blobpool + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/billy" +) + +// TestLimboLegacyMigration checks that a limbo entry in the legacy limboBlob +// layout is flagged for conversion by newLimbo instead of being dropped, so the +// conversion queue can migrate it to the blobTxForPool layout. +func TestLimboLegacyMigration(t *testing.T) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 10, 100, 100, 2, 0, key) + + dir := t.TempDir() + + // Write a single entry using the legacy on-disk layout. + store, err := billy.Open(billy.Options{Path: dir}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil) + if err != nil { + t.Fatalf("failed to open store: %v", err) + } + legacy := struct { + TxHash common.Hash + Block uint64 + Tx *types.Transaction + }{tx.Hash(), 42, tx} + data, err := rlp.EncodeToBytes(&legacy) + if err != nil { + t.Fatalf("failed to encode legacy entry: %v", err) + } + id, err := store.Put(data) + if err != nil { + t.Fatalf("failed to store legacy entry: %v", err) + } + store.Close() + + // Open the limbo, which should flag the legacy entry for conversion. + l, convert, err := newLimbo(new(params.ChainConfig), dir) + if err != nil { + t.Fatalf("failed to open limbo: %v", err) + } + defer l.Close() + + if len(convert) != 1 || convert[0] != id { + t.Fatalf("legacy entry not flagged for conversion: got %v, want [%d]", convert, id) + } +} diff --git a/core/txpool/locals/journal.go b/core/txpool/locals/journal.go index cd2be8a794..aac59bd91f 100644 --- a/core/txpool/locals/journal.go +++ b/core/txpool/locals/journal.go @@ -119,10 +119,11 @@ func (journal *journal) load(add func([]*types.Transaction) []error) error { func (journal *journal) setupWriter() error { if journal.writer != nil { - if err := journal.writer.Close(); err != nil { + err := journal.writer.Close() + journal.writer = nil + if err != nil { return err } - journal.writer = nil } // Re-open the journal file for appending @@ -152,10 +153,11 @@ func (journal *journal) insert(tx *types.Transaction) error { func (journal *journal) rotate(all map[common.Address]types.Transactions) error { // Close the current journal (if any is open) if journal.writer != nil { - if err := journal.writer.Close(); err != nil { + err := journal.writer.Close() + journal.writer = nil + if err != nil { return err } - journal.writer = nil } // Generate a new journal with the contents of the current pool replacement, err := os.OpenFile(journal.path+".new", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) diff --git a/core/txpool/validation.go b/core/txpool/validation.go index fa148797ef..a45f60ea78 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) var ( @@ -113,16 +114,22 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return core.ErrTipAboveFeeCap } // Make sure the transaction is signed properly - if _, err := types.Sender(signer, tx); err != nil { + from, err := types.Sender(signer, tx) + if err != nil { return fmt.Errorf("%w: %v", ErrInvalidSender, err) } // Limit nonce to 2^64-1 per EIP-2681 if tx.Nonce()+1 < tx.Nonce() { return core.ErrNonceMax } + // Sanity check for extremely large numbers (supported by RLP or RPC) + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return core.ErrInsufficientFunds + } // 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, rules, params.CostPerStateByte) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), from, tx.To(), value, rules, params.CostPerStateByte) if err != nil { return err } @@ -131,7 +138,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(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, from, tx.To(), value, tx.Data(), tx.AccessList()) if err != nil { return err } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 522192cee8..db891b92d0 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -80,7 +80,7 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // according to the spec or any code changes are contained which exceed protocol // max code size. func (e *BlockAccessList) Validate(blockGasLimit uint64, blockTxCount int) error { - if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { + if !isStrictlySortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") diff --git a/core/types/bloom9.go b/core/types/bloom9.go index 1d57e8e4bc..1b6c961c61 100644 --- a/core/types/bloom9.go +++ b/core/types/bloom9.go @@ -110,9 +110,9 @@ func CreateBloom(receipt *Receipt) Bloom { buf [6]byte ) for _, log := range receipt.Logs { - bin.AddWithBuffer(log.Address.Bytes(), &buf) - for _, b := range log.Topics { - bin.AddWithBuffer(b[:], &buf) + bin.AddWithBuffer(log.Address[:], &buf) + for k := range log.Topics { + bin.AddWithBuffer(log.Topics[k][:], &buf) } } return bin diff --git a/core/types/log.go b/core/types/log.go index 487ca57b5a..3ea1e0db7c 100644 --- a/core/types/log.go +++ b/core/types/log.go @@ -79,17 +79,3 @@ func EthTransferLog(from, to common.Address, amount *uint256.Int) *Log { Data: amount32[:], } } - -// EthBurnLog creates an ETH burn log according to EIP-7708. -// Specification: https://eips.ethereum.org/EIPS/eip-7708 -func EthBurnLog(from common.Address, amount *uint256.Int) *Log { - amount32 := amount.Bytes32() - return &Log{ - Address: params.SystemAddress, - Topics: []common.Hash{ - params.EthBurnLogEvent, - common.BytesToHash(from.Bytes()), - }, - Data: amount32[:], - } -} diff --git a/core/vm/eip8037_test.go b/core/vm/eip8037_test.go index 658096d228..0eafc3e4af 100644 --- a/core/vm/eip8037_test.go +++ b/core/vm/eip8037_test.go @@ -45,9 +45,6 @@ var ( func amsterdam8037Config() *params.ChainConfig { cfg := *params.MergedTestChainConfig cfg.AmsterdamTime = new(uint64) - blob := *cfg.BlobScheduleConfig - blob.Amsterdam = blob.Osaka - cfg.BlobScheduleConfig = &blob return &cfg } @@ -193,16 +190,25 @@ func TestSStoreChargedAtOpcodeEnd(t *testing.T) { } // The SSTORE reentrancy sentry checks gas_left only; the reservoir is excluded. -// Uses a noop write (1->1->1) so the sentry is the sole gate. +// Uses a noop write (1->1->1): the two PUSH1s cost 6, leaving gas_left at the +// sentry (2300) for a 2306 budget. Under EIP-8038 the cold-slot access that +// follows a cleared sentry costs COLD_STORAGE_ACCESS (3000). func TestSStoreStipendExcludesReservoir(t *testing.T) { - // regular at the sentry, huge reservoir: must still fail. + // regular at the sentry, huge reservoir: must still fail, proving the + // reservoir does not count toward the sentry. if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2306, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil { t.Fatal("expected sentry failure with regular gas at the limit") } - // one more regular gas clears the sentry. - if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2307, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil { + // Enough regular gas to clear the sentry and pay the cold-slot access + // (6 for the PUSH1s + COLD_STORAGE_ACCESS) succeeds with a huge reservoir. + regular := 6 + params.ColdStorageAccessAmsterdam + if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil { t.Fatalf("unexpected failure above sentry: %v", err) } + // One gas short of the cold-slot access still fails (now on OOG, not sentry). + if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular-1, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil { + t.Fatal("expected OOG when regular gas cannot cover cold-slot access") + } } // ---- CALL / CREATE bytecode helpers ---- diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go new file mode 100644 index 0000000000..f20a9d8a0e --- /dev/null +++ b/core/vm/eip8038_test.go @@ -0,0 +1,254 @@ +// 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 . + +// Opcode-level tests for EIP-8038 (state-access gas cost update). They reuse the +// Amsterdam harness from eip8037_test.go and assert the re-priced regular-gas, +// state-gas and refund-counter accounting. + +package vm + +import ( + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "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/params" + "github.com/holiman/uint256" +) + +// run8038 executes code at a contract address under the Amsterdam ruleset and +// returns the resulting budget together with the transaction's refund counter. +func run8038(t *testing.T, code []byte, gas GasBudget, value *uint256.Int, setup func(*state.StateDB, common.Address)) (GasBudget, uint64, error) { + t.Helper() + self := common.BytesToAddress([]byte("self")) + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.CreateAccount(self) + statedb.SetCode(self, code, tracing.CodeChangeUnspecified) + if setup != nil { + setup(statedb, self) + } + statedb.Finalise(true) + _, result, err := amsterdam8037EVM(statedb).Call(common.Address{}, self, nil, gas, value) + return result, statedb.GetRefund(), err +} + +// TestEIP8038SStore exercises SSTORE under Amsterdam (EIP-8037 + EIP-8038), +// asserting the two-dimensional charge (regular + state gas) and the net refund +// counter. It covers single stores in isolation (the EIP-8038 cases-table rows, +// cold access), the warm-access variants, the dirty-slot refund reversals and +// multi-store round trips. +// +// Each sstore() is "PUSH1 val; PUSH1 slot; SSTORE", so the non-SSTORE overhead is +// 6 gas (two PUSH1) per store. The first store to a slot is cold and the rest are +// warm, so the access component is COLD_STORAGE_ACCESS + (n-1) * WARM_ACCESS for n +// stores. STORAGE_WRITE is charged once per "first change" (current == original). +// GAS_STORAGE_SET is state gas, charged when a slot is created from zero and +// refilled to the reservoir when that creation is undone within the same tx. +func TestEIP8038SStore(t *testing.T) { + const ( + push = uint64(6) // two PUSH1 per SSTORE + cold = params.ColdStorageAccessAmsterdam + warm = params.WarmStorageReadCostEIP2929 + write = params.StorageWriteAmsterdam + clear = params.StorageClearRefundAmsterdam + ) + set := uint64(params.StorageCreationSize * params.CostPerStateByte) // GAS_STORAGE_SET + + // access(n) is the access-only regular cost for n stores: cold first, warm rest. + access := func(n uint64) uint64 { return cold + (n-1)*warm } + + cases := []struct { + name string + orig byte // committed (pre-tx) value; 0 means a fresh slot + vals []byte // values written to slot 0, in order + wantReg uint64 + wantState int64 + wantRfnd uint64 + }{ + // Single store, cold access (EIP-8038 cases table, Cold rows + noop). + {"noop (1->1)", 1, []byte{1}, push + cold, 0, 0}, + {"create (0->1)", 0, []byte{1}, push + cold + write, int64(set), 0}, + {"first change (1->2)", 1, []byte{2}, push + cold + write, 0, 0}, + {"clear (1->0)", 1, []byte{0}, push + cold + write, 0, clear}, + // Two stores, warm access on the second (Warm rows of the cases table). + {"create warm (0->0->1)", 0, []byte{0, 1}, 2*push + access(2) + write, int64(set), 0}, + {"first change warm (1->1->2)", 1, []byte{1, 2}, 2*push + access(2) + write, 0, 0}, + {"clear warm (1->1->0)", 1, []byte{1, 0}, 2*push + access(2) + write, 0, clear}, + {"dirty modified again (1->2->3)", 1, []byte{2, 3}, 2*push + access(2) + write, 0, 0}, + // Two stores, refund reversals when a slot returns toward its original. + {"reset to zero (0->1->0)", 0, []byte{1, 0}, 2*push + access(2) + write, 0, write}, + {"reset to original (1->2->1)", 1, []byte{2, 1}, 2*push + access(2) + write, 0, write}, + {"cleared then restored (1->0->1)", 1, []byte{0, 1}, 2*push + access(2) + write, 0, write}, + {"cleared then new value (1->0->2)", 1, []byte{0, 2}, 2*push + access(2) + write, 0, 0}, + // Three stores, round trips (note the state-gas refill on the 0-> path). + {"0->1->0->1", 0, []byte{1, 0, 1}, 3*push + access(3) + 2*write, int64(set), write}, + {"1->0->1->0", 1, []byte{0, 1, 0}, 3*push + access(3) + 2*write, 0, clear + write}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var code []byte + for _, v := range tc.vals { + code = append(code, sstore(0, v)...) + } + var setup func(*state.StateDB, common.Address) + if tc.orig != 0 { + setup = setSlot(0, tc.orig) + } + res, refund, err := run8038(t, code, hugeBudget(), new(uint256.Int), setup) + if err != nil { + t.Fatal(err) + } + if res.UsedRegularGas != tc.wantReg { + t.Errorf("regular gas = %d, want %d", res.UsedRegularGas, tc.wantReg) + } + if res.UsedStateGas != tc.wantState { + t.Errorf("state gas = %d, want %d", res.UsedStateGas, tc.wantState) + } + if refund != tc.wantRfnd { + t.Errorf("refund = %d, want %d", refund, tc.wantRfnd) + } + }) + } +} + +// TestEIP8038SLoad checks the re-priced SLOAD access costs (cold 3000, warm 100). +func TestEIP8038SLoad(t *testing.T) { + push := uint64(3) // PUSH1 slot + // PUSH1 0x00; SLOAD + cold := []byte{0x60, 0x00, 0x54} + res, _, err := run8038(t, cold, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push + params.ColdStorageAccessAmsterdam; res.UsedRegularGas != want { + t.Fatalf("cold SLOAD = %d, want %d", res.UsedRegularGas, want) + } + // PUSH1 0x00; SLOAD; PUSH1 0x00; SLOAD -> second access is warm. + warm := []byte{0x60, 0x00, 0x54, 0x60, 0x00, 0x54} + res, _, err = run8038(t, warm, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + want := 2*push + params.ColdStorageAccessAmsterdam + params.WarmStorageReadCostEIP2929 + if res.UsedRegularGas != want { + t.Fatalf("cold+warm SLOAD = %d, want %d", res.UsedRegularGas, want) + } +} + +// TestEIP8038AccountAccess checks the re-priced cold-account access for the +// account-reading opcodes and the extra WARM_ACCESS surcharge for EXTCODESIZE +// and EXTCODECOPY (their second database read). +func TestEIP8038AccountAccess(t *testing.T) { + push20 := uint64(3) + addr := common.BytesToAddress([]byte("some-cold-account")) + + // pushAddr emits PUSH20 . + pushAddr := func() []byte { return append([]byte{0x73}, addr.Bytes()...) } + + cold := params.ColdAccountAccessAmsterdam + warm := params.WarmStorageReadCostEIP2929 + + t.Run("BALANCE", func(t *testing.T) { + code := append(pushAddr(), 0x31) // BALANCE + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold; res.UsedRegularGas != want { + t.Fatalf("cold BALANCE = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODEHASH", func(t *testing.T) { + code := append(pushAddr(), 0x3f) // EXTCODEHASH + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODEHASH = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODESIZE adds WARM_ACCESS", func(t *testing.T) { + code := append(pushAddr(), 0x3b) // EXTCODESIZE + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold + warm; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODESIZE = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODECOPY adds WARM_ACCESS", func(t *testing.T) { + // PUSH1 0 (length); PUSH1 0 (codeOffset); PUSH1 0 (destOffset); PUSH20 addr; EXTCODECOPY + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00} + code = append(code, pushAddr()...) + code = append(code, 0x3c) // EXTCODECOPY + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + // three PUSH1 + one PUSH20 = 12 gas, zero-length copy => no memory/copy gas. + if want := uint64(12) + cold + warm; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODECOPY = %d, want %d", res.UsedRegularGas, want) + } + }) +} + +// TestEIP8038SelfdestructAccountWrite checks that SELFDESTRUCT sending a positive +// balance to an empty account is charged the cold access, an additional +// ACCOUNT_WRITE (regular) and GAS_NEW_ACCOUNT (state). +func TestEIP8038SelfdestructAccountWrite(t *testing.T) { + beneficiary := common.BytesToAddress([]byte("fresh-beneficiary")) + // PUSH20 beneficiary; SELFDESTRUCT + code := append([]byte{0x73}, beneficiary.Bytes()...) + code = append(code, 0xff) + + // Fund the contract so it sends a positive balance on self-destruct. + fundSelf := func(db *state.StateDB, self common.Address) { + db.AddBalance(self, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + } + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), fundSelf) + if err != nil { + t.Fatal(err) + } + const push20 = uint64(3) + wantReg := push20 + params.SelfdestructGasEIP150 + params.ColdAccountAccessAmsterdam + params.AccountWriteAmsterdam + if res.UsedRegularGas != wantReg { + t.Fatalf("regular gas = %d, want %d", res.UsedRegularGas, wantReg) + } + if want := int64(params.AccountCreationSize * params.CostPerStateByte); res.UsedStateGas != want { + t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want) + } +} + +// TestEIP8038SStoreAccessGuard covers the affordability check that bails out +// before the slot is read once the gas left cannot cover the slot's access cost. +// The two PUSH1s cost 6, so a 2506 budget leaves 2500 at the SSTORE: above the +// reentrancy sentry (2300) yet below COLD_STORAGE_ACCESS (3000). The guard must +// fire, distinguishable from the sentry/charge OOG by its "slot access" message. +func TestEIP8038SStoreAccessGuard(t *testing.T) { + budget := NewGasBudget(6+params.SstoreSentryGasEIP2200+200, 0) + _, _, err := run8038(t, sstore(0, 1), budget, new(uint256.Int), nil) + if err == nil { + t.Fatal("expected failure: gas left cannot cover cold-slot access") + } + if !strings.Contains(err.Error(), "not enough gas for slot access") { + t.Fatalf("got %q, want the slot-access guard error", err) + } +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 814e4be788..8a09856029 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -43,7 +43,8 @@ var activators = map[int]func(*JumpTable){ 7939: enable7939, 8024: enable8024, 7843: enable7843, - 8037: enable8037, + 8037: enable8037And8038, + 8038: enable8037And8038, } // EnableEIP enables the given EIP on the config. @@ -584,13 +585,30 @@ func enable7843(jt *JumpTable) { } } -// enable8037 enables the multidimensional-metering as specified in EIP-8037. -func enable8037(jt *JumpTable) { - jt[CREATE].constantGas = params.CreateGasAmsterdam +// enable8037And8038 enables EIP-8037 (multidimensional state-gas metering) +// together with EIP-8038 (state-access gas cost update). +func enable8037And8038(jt *JumpTable) { + jt[CREATE].constantGas = params.CreateAccessAmsterdam jt[CREATE].dynamicGas = gasCreateEip8037 - jt[CREATE2].constantGas = params.CreateGasAmsterdam + jt[CREATE2].constantGas = params.CreateAccessAmsterdam jt[CREATE2].dynamicGas = gasCreate2Eip8037 - jt[CALL].dynamicGas = gasCallEIP8037 - jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037 - jt[SSTORE].dynamicGas = gasSStore8037 + + // Storage-access opcodes + jt[SLOAD].dynamicGas = gasSLoad8038 + jt[SSTORE].dynamicGas = gasSStore8037And8038 + + // Account-access opcodes + jt[BALANCE].dynamicGas = gasEip8038AccountCheck + jt[EXTCODEHASH].dynamicGas = gasEip8038AccountCheck + jt[EXTCODESIZE].dynamicGas = gasExtCodeSize8038 + jt[EXTCODECOPY].dynamicGas = gasExtCodeCopy8038 + + // Call family + jt[CALL].dynamicGas = gasCall8038 + jt[CALLCODE].dynamicGas = gasCallCode8038 + jt[STATICCALL].dynamicGas = gasStaticCall8038 + jt[DELEGATECALL].dynamicGas = gasDelegateCall8038 + + // SELFDESTRUCT + jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037And8038 } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 4bd971e711..d8fbf2b461 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -478,6 +478,28 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor return gas, nil } +// gasCallCodeIntrinsic8038 mirrors gasCallCodeIntrinsic but charges the +// re-priced CALL_VALUE (ACCOUNT_WRITE + CALL_STIPEND) on value transfers per +// EIP-8038. CALLCODE executes in the caller's context, so it never creates a +// new account and has no state-gas component. +func gasCallCodeIntrinsic8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + memoryGas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + var ( + gas uint64 + overflow bool + ) + if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { + gas += params.CallValueTransferAmsterdam + } + if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { @@ -576,10 +598,11 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, }, nil } -// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam. -// It computes memory expansion + value transfer gas but excludes new account -// creation, which is handled as state gas by the wrapper. -func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +// regularGasCall8038 is the intrinsic regular-gas calculator for CALL in +// Amsterdam. It computes memory expansion plus the re-priced CALL_VALUE +// (ACCOUNT_WRITE + CALL_STIPEND) on value transfers, but excludes new account +// creation, which is handled as state gas by stateGasCall8037. +func regularGasCall8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.back(2).IsZero() @@ -593,7 +616,7 @@ func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } var transferGas uint64 if transfersValue && !evm.chainRules.IsEIP4762 { - transferGas = params.CallValueTransferGas + transferGas = params.CallValueTransferAmsterdam } var overflow bool if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { @@ -630,7 +653,9 @@ func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error return gas, nil } -func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +// gasSelfdestruct8037And8038 implements the SELFDESTRUCT gas charging under +// the EIP8037 and EIP-8038. +func gasSelfdestruct8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { return GasCosts{}, ErrWriteProtection } @@ -639,11 +664,11 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory address = common.Address(stack.peek().Bytes20()) ) if !evm.StateDB.AddressInAccessList(address) { - // If the caller cannot afford the cost, this change will be rolled back + // If the caller cannot afford the cost, this change will be rolled back. evm.StateDB.AddAddressToAccessList(address) - gas.RegularGas = params.ColdAccountAccessCostEIP2929 + gas.RegularGas = params.ColdAccountAccessAmsterdam } - // Check we have enough regular gas before we add the address to the BAL + // Check we have enough regular gas before we add the address to the BAL. if contract.Gas.RegularGas < gas.RegularGas { return gas, ErrOutOfGas } @@ -654,79 +679,68 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory // // Funding such an account makes it permanent state growth and must be charged. if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas.RegularGas += params.AccountWriteAmsterdam gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte } return gas, nil } -func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +// gasSStore8037And8038 implements the SSTORE gas charging under EIP-8037 and +// EIP-8038. +func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { return GasCosts{}, ErrWriteProtection } - // If we fail the minimum gas availability invariant, fail (0) + // If we fail the minimum gas availability invariant, fail (0). if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { return GasCosts{}, errors.New("not enough gas for reentrancy sentry") } - // Gas sentry honoured, do the actual gas calculation based on the stored value var ( - y, x = stack.back(1), stack.peek() - slot = common.Hash(x.Bytes32()) - current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) - cost GasCosts + y, x = stack.back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + value = common.Hash(y.Bytes32()) + stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte ) // Check slot presence in the access list + access := params.WarmStorageReadCostEIP2929 if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { - cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929} - // If the caller cannot afford the cost, this change will be rolled back + access = params.ColdStorageAccessAmsterdam evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } - value := common.Hash(y.Bytes32()) + // Check access cost affordability before reading slot + if contract.Gas.RegularGas < access { + return GasCosts{}, errors.New("not enough gas for slot access") + } + // Read the slot value for gas cost measurement + current, original := evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) if current == value { // noop (1) - // EIP 2200 original clause: - // return params.SloadGasEIP2200, nil - return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS + return GasCosts{RegularGas: access}, nil } - if original == current { + if original == current { // first change of the slot (2.1) if original == (common.Hash{}) { // create slot (2.1.1) return GasCosts{ - RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929, - StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte, + RegularGas: access + params.StorageWriteAmsterdam, + StateGas: stateSet, }, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) - evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam) } - // EIP-2200 original clause: - // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) - return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil // write existing slot (2.1.2) + return GasCosts{RegularGas: access + params.StorageWriteAmsterdam}, nil // write existing slot (2.1.2) } if original != (common.Hash{}) { if current == (common.Hash{}) { // recreate slot (2.2.1.1) - evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529) + evm.StateDB.SubRefund(params.StorageClearRefundAmsterdam) } else if value == (common.Hash{}) { // delete slot (2.2.1.2) - evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam) } } - if original == value { + if original == value { // reset to original value (2.2.2) if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) - // EIP-8037 point (2): refund state gas directly to the reservoir - // at the SSTORE restoration point (0→x→0 in same tx); not to the - // refund counter, which is capped at gas_used/5. - contract.Gas.RefundState(params.StorageCreationSize * evm.Context.CostPerStateByte) - - // Regular portion of the refund still goes through the refund counter. - evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) - } else { // reset to original existing slot (2.2.2.2) - // EIP 2200 Original clause: - // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) - // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST) - // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST - // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST - evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) + contract.Gas.RefundState(stateSet) } + evm.StateDB.AddRefund(params.StorageWriteAmsterdam) } - // EIP-2200 original clause: - //return params.SloadGasEIP2200, nil // dirty update (2.2) - return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2) + return GasCosts{RegularGas: access}, nil // dirty update (2.2) } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 10b8eeb033..76a2c7af60 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -196,6 +196,22 @@ func (g *GasBudget) RefundState(s uint64) { g.UsedStateGas -= int64(s) } +// RefundStateToReservoir credits a state-gas refund directly to the +// reservoir, without repaying spilled regular gas first. +// +// Per the spec's set_delegation, authorization refunds (and the post-create +// new-account refund) are added to message.state_gas_reservoir directly, in +// contrast to the LIFO inline refunds handled by RefundState. The usage +// counter is decremented by the full amount, matching the spec's +// tx_state_gas = intrinsic_state + state_gas_used - state_refund and +// preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + Spilled +func (g *GasBudget) RefundStateToReservoir(s uint64) { + g.StateGas += s + g.UsedStateGas -= int64(s) +} + // Forward drains `regular` regular gas and the entire state reservoir from // the parent's running budget and returns the initial GasBudget for a child // frame. The parent's UsedRegularGas is bumped by the forwarded amount so diff --git a/core/vm/instructions.go b/core/vm/instructions.go index b20cce4c0d..328623848e 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -928,8 +928,15 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro if newContract { if this != beneficiary { // Skip no-op transfer when self-destructing to self. evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) + } else if !evm.chainRules.IsAmsterdam { + // Self-destructing to self burns the balance prior to EIP-8246. + // EIP-8246 (Amsterdam) removes this burn: the balance is left + // untouched and the account is preserved as a balance-only account + // at transaction finalization (unless its balance is zero, in which + // case EIP-161 deletes it). + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) } - evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.SelfDestruct(this) } @@ -938,12 +945,10 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) } - if evm.chainRules.IsAmsterdam && !balance.IsZero() { - if this != beneficiary { - evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) - } else if newContract { - evm.StateDB.AddLog(types.EthBurnLog(this, balance)) - } + // EIP-7708: emit a transfer log for the moved balance. EIP-8246 removes the + // SELFDESTRUCT burn entirely, so there is no longer a burn to log. + if evm.chainRules.IsAmsterdam && !balance.IsZero() && this != beneficiary { + evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) } if tracer := evm.Config.Tracer; tracer != nil { diff --git a/core/vm/interface.go b/core/vm/interface.go index a9938c2a28..5bba39069c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -90,7 +90,6 @@ type StateDB interface { Snapshot() int AddLog(*types.Log) - LogsForBurnAccounts() []*types.Log AddPreimage(common.Hash, []byte) Witness() *stateless.Witness diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 5cc5e34ced..6b931083fe 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -99,9 +99,9 @@ func newVerkleInstructionSet() JumpTable { func newAmsterdamInstructionSet() JumpTable { instructionSet := newOsakaInstructionSet() - enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) - enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) - enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase) + enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) + enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + enable8037And8038(&instructionSet) // EIP-8037 (state-gas metering) + EIP-8038 (state-access repricing) return validate(instructionSet) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 8b2a627fef..3669626d5a 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -101,16 +101,25 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { loc := stack.peek() slot := common.Hash(loc.Bytes32()) - // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { - // If the caller cannot afford the cost, this change will be rolled back - // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) return GasCosts{RegularGas: params.ColdSloadCostEIP2929}, nil } return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil } +// gasSLoad8038 mirrors gasSLoadEIP2929 but uses the EIP-8038 COLD_STORAGE_ACCESS +// for a cold slot. +func gasSLoad8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + loc := stack.peek() + slot := common.Hash(loc.Bytes32()) + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + return GasCosts{RegularGas: params.ColdStorageAccessAmsterdam}, nil + } + return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil +} + // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 // EIP spec: // > If the target is not in accessed_addresses, @@ -137,6 +146,34 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo return GasCosts{RegularGas: gas}, nil } +// gasExtCodeCopy8038 mirrors gasExtCodeCopyEIP2929 but uses the EIP-8038 +// COLD_ACCOUNT_ACCESS and adds an extra WARM_ACCESS for the second +// database read EXTCODECOPY performs. +func gasExtCodeCopy8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + // memory expansion first (dynamic part of pre-2929 implementation) + gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) + if err != nil { + return GasCosts{}, err + } + gas := gasCost.RegularGas + addr := common.Address(stack.peek().Bytes20()) + // Check slot presence in the access list + if !evm.StateDB.AddressInAccessList(addr) { + evm.StateDB.AddAddressToAccessList(addr) + var overflow bool + // We charge (cold-warm), since 'warm' is already charged as constantGas + if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessAmsterdam-params.WarmStorageReadCostEIP2929); overflow { + return GasCosts{}, ErrGasUintOverflow + } + } + // Additional WARM_ACCESS for the second database read (contract code). + var overflow bool + if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow { + return GasCosts{}, ErrGasUintOverflow + } + return GasCosts{RegularGas: gas}, nil +} + // gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list. // If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it // is also using 'warm' as constant factor. @@ -156,6 +193,32 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem return GasCosts{}, nil } +// gasEip8038AccountCheck mirrors gasEip2929AccountCheck but uses the EIP-8038 +// COLD_ACCOUNT_ACCESS. Used by BALANCE and EXTCODEHASH. +func gasEip8038AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + addr := common.Address(stack.peek().Bytes20()) + // Check slot presence in the access list + if !evm.StateDB.AddressInAccessList(addr) { + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddAddressToAccessList(addr) + // The warm storage read cost is already charged as constantGas + return GasCosts{RegularGas: params.ColdAccountAccessAmsterdam - params.WarmStorageReadCostEIP2929}, nil + } + return GasCosts{}, nil +} + +// gasExtCodeSize8038 prices EXTCODESIZE under EIP-8038: the gasEip8038AccountCheck +// surcharge plus an additional WARM_ACCESS for the second database read (code size). +func gasExtCodeSize8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + cost, err := gasEip8038AccountCheck(evm, contract, stack, mem, memorySize) + if err != nil { + return GasCosts{}, err + } + // Additional WARM_ACCESS for the second database read (contract size). + cost.RegularGas += params.WarmStorageReadCostEIP2929 + return cost, nil +} + func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { addr := common.Address(stack.back(addressPosition).Bytes20()) @@ -257,12 +320,19 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { return gasFunc } +// recordDelegationAccess records the EIP-7702 delegated target in the block +// access list (EIP-7928). +func recordDelegationAccess(evm *EVM, target common.Address) { + if evm.chainRules.IsAmsterdam { + evm.StateDB.GetCode(target) + } +} + var ( - innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic) - gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic) - gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic) - gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic) - innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037) + innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic, params.ColdAccountAccessCostEIP2929) ) func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -277,15 +347,24 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) } -func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +var ( + innerGasCall8038 = makeCallVariantGasCallEIP8037(regularGasCall8038, stateGasCall8037, params.ColdAccountAccessAmsterdam) + gasCallCode8038 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic8038, params.ColdAccountAccessAmsterdam) + gasDelegateCall8038 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessAmsterdam) + gasStaticCall8038 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessAmsterdam) +) + +// gasCall8038 prices CALL for Amsterdam, guarding against value transfers in a +// read-only context before delegating to the state-gas-aware wrapper. +func gasCall8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { transfersValue := !stack.back(2).IsZero() if evm.readOnly && transfersValue { return GasCosts{}, ErrWriteProtection } - return innerGasCallEIP8037(evm, contract, stack, mem, memorySize) + return innerGasCall8038(evm, contract, stack, mem, memorySize) } -func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { +func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -300,7 +379,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // The WarmStorageReadCostEIP2929 (100) is already deducted in the form // of a constant cost, so the cost to charge for cold access, if any, // is Cold - Warm - eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929 // Charge the remaining difference here already, to correctly calculate // available gas for call @@ -321,7 +400,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. // It's an essential safeguard before any stateful check. - if contract.Gas.RegularGas < intrinsicCost { + if !contract.chargeRegular(intrinsicCost, evm.Config.Tracer, tracing.GasChangeIgnored) { return GasCosts{}, ErrOutOfGas } @@ -331,15 +410,19 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { eip7702Cost = params.WarmStorageReadCostEIP2929 } else { evm.StateDB.AddAddressToAccessList(target) - eip7702Cost = params.ColdAccountAccessCostEIP2929 + eip7702Cost = coldCost } if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0)) if err != nil { return GasCosts{}, err } @@ -347,11 +430,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // adding it to the return, it will be charged outside of this function, as // part of the dynamic gas. This will ensure it is correctly reported to // tracers. - contract.Gas.RegularGas += eip2929Cost + eip7702Cost + contract.Gas.RegularGas += eip2929Cost + eip7702Cost + intrinsicCost + // Undo the RegularGasUsed increments from the direct UseGas charges, // since this gas will be re-charged via the returned cost. contract.Gas.UsedRegularGas -= eip2929Cost contract.Gas.UsedRegularGas -= eip7702Cost + contract.Gas.UsedRegularGas -= intrinsicCost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls. @@ -376,7 +461,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // It extends the EIP-7702 pattern with state gas handling and GasUsed tracking. // intrinsicFunc computes the regular gas (memory + transfer, no new account creation). // stateGasFunc computes the state gas (new account creation as state gas). -func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc { +func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc, coldCost uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -386,7 +471,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat // EIP-2929 cold access check. if !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr) - eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929 if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } @@ -411,11 +496,15 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat eip7702Cost = params.WarmStorageReadCostEIP2929 } else { evm.StateDB.AddAddressToAccessList(target) - eip7702Cost = params.ColdAccountAccessCostEIP2929 + eip7702Cost = coldCost } if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Compute and charge state gas (new account creation) AFTER regular gas. diff --git a/eth/backend.go b/eth/backend.go index ef4f593e6a..fbaef000af 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -281,6 +281,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.OverrideOsaka != nil { overrides.OverrideOsaka = config.OverrideOsaka } + if config.OverrideAmsterdam != nil { + overrides.OverrideAmsterdam = config.OverrideAmsterdam + } if config.OverrideBPO1 != nil { overrides.OverrideBPO1 = config.OverrideBPO1 } diff --git a/eth/catalyst/api_benchmark_test.go b/eth/catalyst/api_benchmark_test.go index ee0a0a4888..6d6ad59f7f 100644 --- a/eth/catalyst/api_benchmark_test.go +++ b/eth/catalyst/api_benchmark_test.go @@ -197,7 +197,6 @@ func newBenchmarkBlobEnv(b *testing.B, blobCount int, version byte, fork benchFo config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ Cancun: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 3338477}, Prague: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, - Osaka: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, } // Configure fork times based on requested fork switch fork { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 99a0fa89f7..378bb64a1e 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -206,6 +206,9 @@ type Config struct { // OverrideOsaka (TODO: remove after the fork) OverrideOsaka *uint64 `toml:",omitempty"` + // OverrideAmsterdam (TODO: remove after the fork) + OverrideAmsterdam *uint64 `toml:",omitempty"` + // OverrideBPO1 (TODO: remove after the fork) OverrideBPO1 *uint64 `toml:",omitempty"` diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 61d6ccb168..b24cde8a6a 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -64,6 +64,7 @@ func (c Config) MarshalTOML() (interface{}, error) { RPCEVMTimeout time.Duration RPCTxFeeCap float64 OverrideOsaka *uint64 `toml:",omitempty"` + OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO2 *uint64 `toml:",omitempty"` OverrideUBT *uint64 `toml:",omitempty"` @@ -119,6 +120,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCTxFeeCap = c.RPCTxFeeCap enc.OverrideOsaka = c.OverrideOsaka + enc.OverrideAmsterdam = c.OverrideAmsterdam enc.OverrideBPO1 = c.OverrideBPO1 enc.OverrideBPO2 = c.OverrideBPO2 enc.OverrideUBT = c.OverrideUBT @@ -178,6 +180,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RPCEVMTimeout *time.Duration RPCTxFeeCap *float64 OverrideOsaka *uint64 `toml:",omitempty"` + OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO2 *uint64 `toml:",omitempty"` OverrideUBT *uint64 `toml:",omitempty"` @@ -330,6 +333,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideOsaka != nil { c.OverrideOsaka = dec.OverrideOsaka } + if dec.OverrideAmsterdam != nil { + c.OverrideAmsterdam = dec.OverrideAmsterdam + } if dec.OverrideBPO1 != nil { c.OverrideBPO1 = dec.OverrideBPO1 } diff --git a/eth/handler_eth.go b/eth/handler_eth.go index cb5e3d92a6..79fd1d118d 100644 --- a/eth/handler_eth.go +++ b/eth/handler_eth.go @@ -104,7 +104,7 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error { // handleTransactions marks all given transactions as known to the peer // and performs basic validations. func handleTransactions(peer *eth.Peer, list []*types.Transaction, directBroadcast bool) error { - seen := make(map[common.Hash]struct{}) + seen := make(map[common.Hash]struct{}, len(list)) for _, tx := range list { if tx.Type() == types.BlobTxType { if directBroadcast { diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index 76f3a828a6..c3b15b999a 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -745,7 +745,7 @@ func testGetBlockAccessLists(t *testing.T, protocol uint) { var ( hashes []common.Hash - expect rlp.RawList[RawBlockAccessList] + expect rlp.RawList[rlp.RawValue] ) for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ { block := backend.chain.GetBlockByNumber(i) @@ -774,6 +774,42 @@ func testGetBlockAccessLists(t *testing.T, protocol uint) { } } +// TestBlockAccessListsUnavailableDecode checks that a BlockAccessLists response +// containing the EIP-8159 unavailability marker (RLP empty string). +func TestBlockAccessListsUnavailableDecode(t *testing.T) { + t.Parallel() + + balRaw := makeTestBAL(t, common.Address{0x11}) + + // Assemble a response the way the serving side does, with the middle + // entry signaled as unavailable. + var list rlp.RawList[rlp.RawValue] + list.AppendRaw(balRaw) + list.AppendRaw(rlp.EmptyString) + list.AppendRaw(balRaw) + + enc, err := rlp.EncodeToBytes(&BlockAccessListPacket{RequestId: 42, List: list}) + if err != nil { + t.Fatalf("failed to encode packet: %v", err) + } + var packet BlockAccessListPacket + if err := rlp.DecodeBytes(enc, &packet); err != nil { + t.Fatalf("failed to decode packet: %v", err) + } + bals, err := packet.List.Items() + if err != nil { + t.Fatalf("failed to decode BAL entries: %v", err) + } + if len(bals) != 3 { + t.Fatalf("wrong entry count: got %d, want 3", len(bals)) + } + for i, want := range [][]byte{balRaw, rlp.EmptyString, balRaw} { + if !bytes.Equal(bals[i], want) { + t.Errorf("entry %d mismatch: got %x, want %x", i, bals[i], want) + } + } +} + type decoder struct { msg []byte } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index e56f41420d..d191587812 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -774,10 +774,10 @@ func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { // serviceGetBlockAccessListsQuery assembles the response to a BAL query. // Unavailable BALs are returned as empty list entries. -func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[RawBlockAccessList] { +func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[rlp.RawValue] { var ( bytes int - bals rlp.RawList[RawBlockAccessList] + bals rlp.RawList[rlp.RawValue] ) for _, hash := range query { if bytes >= softResponseLimit || bals.Len() >= maxBALsServe { @@ -815,7 +815,12 @@ func handleBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { metadata := func() interface{} { hashes := make([]common.Hash, len(bals)) for i := range bals { - hashes[i] = crypto.Keccak256Hash(bals[i].Bytes()) + // Unavailable BALs (signaled by the empty string) are marked + // with the zero hash + if bytes.Equal(bals[i], rlp.EmptyString) { + continue + } + hashes[i] = crypto.Keccak256Hash(bals[i]) } return hashes } diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 6d670b53c2..cd97325cf0 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -296,7 +296,7 @@ func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], } // ReplyBlockAccessLists is the response to GetBlockAccessLists (EIP-8159). -func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[RawBlockAccessList]) error { +func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[rlp.RawValue]) error { return p2p.Send(p.rw, BlockAccessListsMsg, &BlockAccessListPacket{ RequestId: id, List: list, diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go index b8b42204af..46434dc800 100644 --- a/eth/protocols/eth/protocol.go +++ b/eth/protocols/eth/protocol.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/forkid" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/rlp" ) @@ -337,15 +336,14 @@ type GetBlockAccessListsPacket struct { GetBlockAccessListsRequest } -type RawBlockAccessList struct { - rlp.RawList[bal.AccountAccess] -} - -type BlockAccessListResponse []RawBlockAccessList +// BlockAccessListResponse holds one raw entry per requested hash. Entries are +// kept as raw values because, per EIP-8159, the RLP empty string signals an +// unavailable BAL (an empty list is itself a valid BAL). +type BlockAccessListResponse []rlp.RawValue type BlockAccessListPacket struct { RequestId uint64 - List rlp.RawList[RawBlockAccessList] + List rlp.RawList[rlp.RawValue] } func (*StatusPacket) Name() string { return "Status" } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 3c3539dbdb..7bcd83d581 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -250,7 +250,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), eth.blockchain.Config(), evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), eth.blockchain.Config(), evm, block.Number(), block.Time()) if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 88132b4b63..d103b8ef35 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -373,7 +373,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{}) - core.PreExecution(ctx, next.BeaconRoot(), next.ParentHash(), api.backend.ChainConfig(), evm, next.Number(), next.Time()) + core.PreExecution(ctx, next.BeaconRoot(), block.Header(), 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 @@ -523,7 +523,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config ) defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), chainConfig, evm, block.Number(), block.Time()) for i, tx := range block.Transactions() { if err := ctx.Err(); err != nil { @@ -582,7 +582,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), api.backend.ChainConfig(), evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), 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 @@ -754,7 +754,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), chainConfig, evm, block.Number(), block.Time()) for i, tx := range block.Transactions() { // Prepare the transaction for un-traced execution diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 0bf80d77b5..d0a2813381 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -308,6 +308,7 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { Address: log1.Address, Topics: log1.Topics, Data: log1.Data, + Index: hexutil.Uint(log1.Index), Position: hexutil.Uint(len(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Calls)), } t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l) diff --git a/internal/ethapi/logtracer.go b/internal/ethapi/logtracer.go index 54d2d653ea..0b7c27079a 100644 --- a/internal/ethapi/logtracer.go +++ b/internal/ethapi/logtracer.go @@ -80,7 +80,7 @@ func (t *tracer) Hooks() *tracing.Hooks { func (t *tracer) onEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { t.logs = append(t.logs, make([]*types.Log, 0)) - if vm.OpCode(typ) != vm.DELEGATECALL && value != nil && value.Cmp(common.Big0) > 0 { + if vm.OpCode(typ) != vm.DELEGATECALL && vm.OpCode(typ) != vm.CALLCODE && value != nil && value.Cmp(common.Big0) > 0 { t.captureTransfer(from, to, value) } } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 8462194b1d..0da2672756 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -322,7 +322,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, evm.SetPrecompiles(precompiles) } // Run pre-execution system calls - blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)) + blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, parent, sim.chainConfig, evm, header.Number, header.Time)) var allLogs []*types.Log for i, call := range block.Calls { diff --git a/log/handler.go b/log/handler.go index 0b51120e51..30a0aff49c 100644 --- a/log/handler.go +++ b/log/handler.go @@ -7,6 +7,7 @@ import ( "log/slog" "math/big" "reflect" + "slices" "sync" "time" @@ -97,7 +98,7 @@ func (h *TerminalHandler) WithAttrs(attrs []slog.Attr) slog.Handler { wr: h.wr, lvl: h.lvl, useColor: h.useColor, - attrs: append(h.attrs, attrs...), + attrs: append(slices.Clone(h.attrs), attrs...), fieldPadding: make(map[string]int), } } diff --git a/miner/payload_building_test.go b/miner/payload_building_test.go index f8e495cc99..734a5605ba 100644 --- a/miner/payload_building_test.go +++ b/miner/payload_building_test.go @@ -17,6 +17,7 @@ package miner import ( + "bytes" "context" "math/big" "reflect" @@ -26,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" @@ -115,6 +117,7 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine copy(gspec.ExtraData[32:32+common.AddressLength], testBankAddress.Bytes()) e.Authorize(testBankAddress) case *ethash.Ethash: + case *beacon.Beacon: default: t.Fatalf("unexpected consensus engine type: %T", engine) } @@ -194,6 +197,58 @@ func TestBuildPayload(t *testing.T) { } } +// TestBuildPayloadAmsterdamTransition verifies that a locally built payload for +// the first Amsterdam block contains the EIP-7997 deterministic deployment +// factory, i.e. the block-building path applies the same irregular state +// transition as block processing and the resulting block is importable. +func TestBuildPayloadAmsterdamTransition(t *testing.T) { + var ( + db = rawdb.NewMemoryDatabase() + recipient = common.HexToAddress("0xdeadbeef") + ) + config := new(params.ChainConfig) + *config = *params.MergedTestChainConfig + config.AmsterdamTime = new(uint64) + *config.AmsterdamTime = 1 // genesis (t=0) is pre-Amsterdam, the first block crosses the fork + + w, b := newTestWorker(t, config, beacon.New(ethash.NewFaker()), db, 0) + + var ( + beaconRoot = common.Hash{0x01} + slotNum = uint64(1) + ) + payload, err := w.buildPayload(context.Background(), &BuildPayloadArgs{ + Parent: b.chain.CurrentBlock().Hash(), + Timestamp: 1, + FeeRecipient: recipient, + Withdrawals: types.Withdrawals{}, + BeaconRoot: &beaconRoot, + SlotNum: &slotNum, + }, false) + if err != nil { + t.Fatalf("Failed to build payload %v", err) + } + block := payload.empty + if !config.IsAmsterdam(block.Number(), block.Time()) { + t.Fatal("transition block is not an Amsterdam block") + } + // The block must be importable: Process applies EIP-7997 independently, so + // a payload built without the factory would fail the state root check here. + if _, err := b.chain.InsertChain(types.Blocks{block}); err != nil { + t.Fatalf("failed to insert transition block: %v", err) + } + statedb, err := b.chain.StateAt(block.Header()) + if err != nil { + t.Fatalf("failed to open state at transition block: %v", err) + } + if code := statedb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(code, params.DeterministicFactoryCode) { + t.Fatalf("factory code missing from built payload state:\n got %x\nwant %x", code, params.DeterministicFactoryCode) + } + if nonce := statedb.GetNonce(params.DeterministicFactoryAddress); nonce != 1 { + t.Fatalf("factory nonce = %d, want 1", nonce) + } +} + func TestPayloadId(t *testing.T) { t.Parallel() ids := make(map[string]int) diff --git a/miner/stress/main.go b/miner/stress/main.go index aaf0993c37..7a8436b01e 100644 --- a/miner/stress/main.go +++ b/miner/stress/main.go @@ -138,7 +138,10 @@ func makeGenesis(faucets []*ecdsa.PrivateKey) *core.Genesis { blockZero := uint64(0) config.AmsterdamTime = &blockZero - config.BlobScheduleConfig.Amsterdam = ¶ms.BlobConfig{ + // Amsterdam inherits its blob schedule from the most recent BPO; activate BPO1 + // at the same time with the target/max we want exercised by the stress harness. + config.BPO1Time = &blockZero + config.BlobScheduleConfig.BPO1 = ¶ms.BlobConfig{ Target: 14, Max: 21, UpdateFraction: 13739630, diff --git a/miner/worker.go b/miner/worker.go index 01a14b8a02..7f0e11f30a 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -326,7 +326,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, return nil, err } // Run pre-execution system calls - env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)) + env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, parent, miner.chainConfig, env.evm, header.Number, header.Time)) return env, nil } diff --git a/node/config.go b/node/config.go index 77ca78a471..b0f8753a7d 100644 --- a/node/config.go +++ b/node/config.go @@ -198,6 +198,9 @@ type Config struct { // BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call. BatchResponseMaxSize int `toml:",omitempty"` + // HTTPBodyLimit is the maximum size (in bytes) of an HTTP request body. + HTTPBodyLimit int `toml:",omitempty"` + // JWTSecret is the path to the hex-encoded jwt secret. JWTSecret string `toml:",omitempty"` diff --git a/node/defaults.go b/node/defaults.go index 3410fa2ae5..145507ffe9 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -67,6 +67,7 @@ var DefaultConfig = Config{ WSModules: []string{"net", "web3"}, BatchRequestLimit: 1000, BatchResponseMaxSize: 25 * 1000 * 1000, + HTTPBodyLimit: 5 * 1024 * 1024, GraphQLVirtualHosts: []string{"localhost"}, P2P: p2p.Config{ ListenAddr: ":30303", diff --git a/node/jwt_handler.go b/node/jwt_handler.go index 637ae19686..4f85c12367 100644 --- a/node/jwt_handler.go +++ b/node/jwt_handler.go @@ -47,7 +47,7 @@ func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) { strToken string claims jwt.RegisteredClaims ) - if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") { + if auth := r.Header.Get("Authorization"); len(auth) >= 7 && strings.EqualFold(auth[:7], "bearer ") { strToken = strings.TrimPrefix(auth, "Bearer ") } if len(strToken) == 0 { diff --git a/node/node.go b/node/node.go index 7c0d69775c..3dd780f42b 100644 --- a/node/node.go +++ b/node/node.go @@ -392,6 +392,7 @@ func (n *Node) startRPC() error { rpcConfig := rpcEndpointConfig{ batchItemLimit: n.config.BatchRequestLimit, batchResponseSizeLimit: n.config.BatchResponseMaxSize, + httpBodyLimit: n.config.HTTPBodyLimit, } initHttp := func(server *httpServer, port int) error { diff --git a/p2p/discover/v5_talk.go b/p2p/discover/v5_talk.go index dca09870d8..648e091769 100644 --- a/p2p/discover/v5_talk.go +++ b/p2p/discover/v5_talk.go @@ -102,10 +102,10 @@ func (t *talkSystem) handleRequest(id enode.ID, addr netip.AddrPort, req *v5wire }() case <-timeout.C: // Couldn't get it in time, drop the request. + t.dropCount++ if time.Since(t.lastLog) > 5*time.Second { log.Warn("Dropping TALKREQ due to overload", "ndrop", t.dropCount) t.lastLog = time.Now() - t.dropCount++ } case <-t.transport.closeCtx.Done(): // Transport closed, drop the request. diff --git a/p2p/enode/iter.go b/p2p/enode/iter.go index 54c2fc7258..d0ccf984d3 100644 --- a/p2p/enode/iter.go +++ b/p2p/enode/iter.go @@ -356,6 +356,7 @@ func (m *FairMix) AddSource(it Iterator) { defer m.mu.Unlock() if m.closed == nil { + it.Close() return } m.wg.Add(1) diff --git a/params/config.go b/params/config.go index 1d69335f97..7b4493609f 100644 --- a/params/config.go +++ b/params/config.go @@ -69,7 +69,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -105,7 +104,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -141,7 +139,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -177,7 +174,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -235,7 +231,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, }, } @@ -330,7 +325,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, }, } @@ -379,12 +373,6 @@ var ( Max: 9, UpdateFraction: 5007716, } - // DefaultOsakaBlobConfig is the default blob configuration for the Osaka fork. - DefaultOsakaBlobConfig = &BlobConfig{ - Target: 6, - Max: 9, - UpdateFraction: 5007716, - } // DefaultBPO1BlobConfig is the default blob configuration for the BPO1 fork. DefaultBPO1BlobConfig = &BlobConfig{ Target: 10, @@ -413,7 +401,6 @@ var ( DefaultBlobSchedule = &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, } ) @@ -670,7 +657,7 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - Prague: @%-10v blob: (%s)\n", *c.PragueTime, c.BlobScheduleConfig.Prague) } if c.OsakaTime != nil { - banner += fmt.Sprintf(" - Osaka: @%-10v blob: (%s)\n", *c.OsakaTime, c.BlobScheduleConfig.Osaka) + banner += fmt.Sprintf(" - Osaka: @%-10v\n", *c.OsakaTime) } if c.BPO1Time != nil { banner += fmt.Sprintf(" - BPO1: @%-10v blob: (%s)\n", *c.BPO1Time, c.BlobScheduleConfig.BPO1) @@ -688,10 +675,10 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - BPO5: @%-10v blob: (%s)\n", *c.BPO5Time, c.BlobScheduleConfig.BPO5) } if c.AmsterdamTime != nil { - banner += fmt.Sprintf(" - Amsterdam: @%-10v blob: (%s)\n", *c.AmsterdamTime, c.BlobScheduleConfig.Amsterdam) + banner += fmt.Sprintf(" - Amsterdam: @%-10v\n", *c.AmsterdamTime) } if c.UBTTime != nil { - banner += fmt.Sprintf(" - UBT: @%-10v blob: (%s)\n", *c.UBTTime, c.BlobScheduleConfig.UBT) + banner += fmt.Sprintf(" - UBT: @%-10v\n", *c.UBTTime) } banner += fmt.Sprintf("\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n") return banner @@ -713,17 +700,18 @@ func (bc *BlobConfig) String() string { } // BlobScheduleConfig determines target and max number of blobs allow per fork. +// +// From Prague onward, the blob schedule is updated only at BPO (Blob Parameter-Only) +// forks. Named forks such as Osaka or Amsterdam inherit the most recently configured +// BPO entry and must not declare their own BlobConfig. type BlobScheduleConfig struct { - Cancun *BlobConfig `json:"cancun,omitempty"` - Prague *BlobConfig `json:"prague,omitempty"` - Osaka *BlobConfig `json:"osaka,omitempty"` - BPO1 *BlobConfig `json:"bpo1,omitempty"` - BPO2 *BlobConfig `json:"bpo2,omitempty"` - BPO3 *BlobConfig `json:"bpo3,omitempty"` - BPO4 *BlobConfig `json:"bpo4,omitempty"` - BPO5 *BlobConfig `json:"bpo5,omitempty"` - Amsterdam *BlobConfig `json:"amsterdam,omitempty"` - UBT *BlobConfig `json:"ubt,omitempty"` + Cancun *BlobConfig `json:"cancun,omitempty"` + Prague *BlobConfig `json:"prague,omitempty"` + BPO1 *BlobConfig `json:"bpo1,omitempty"` + BPO2 *BlobConfig `json:"bpo2,omitempty"` + BPO3 *BlobConfig `json:"bpo3,omitempty"` + BPO4 *BlobConfig `json:"bpo4,omitempty"` + BPO5 *BlobConfig `json:"bpo5,omitempty"` } // IsHomestead returns whether num is either equal to the homestead block or greater. @@ -1000,13 +988,11 @@ func (c *ChainConfig) CheckConfigForkOrder() error { }{ {name: "cancun", timestamp: c.CancunTime, config: bsc.Cancun}, {name: "prague", timestamp: c.PragueTime, config: bsc.Prague}, - {name: "osaka", timestamp: c.OsakaTime, config: bsc.Osaka}, {name: "bpo1", timestamp: c.BPO1Time, config: bsc.BPO1}, {name: "bpo2", timestamp: c.BPO2Time, config: bsc.BPO2}, {name: "bpo3", timestamp: c.BPO3Time, config: bsc.BPO3}, {name: "bpo4", timestamp: c.BPO4Time, config: bsc.BPO4}, {name: "bpo5", timestamp: c.BPO5Time, config: bsc.BPO5}, - {name: "amsterdam", timestamp: c.AmsterdamTime, config: bsc.Amsterdam}, } { if cur.config != nil { if err := cur.config.validate(); err != nil { @@ -1169,28 +1155,33 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { } } -// BlobConfig returns the blob config associated with the provided fork. +// BlobConfig returns the blob config active at the provided fork. Since named +// forks (Osaka, Amsterdam, ...) no longer carry their own blob schedule, the +// lookup walks down from fork through the BPO chain to Prague/Cancun and returns +// the first non-nil entry. func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig { - switch fork { - case forks.BPO5: - return c.BlobScheduleConfig.BPO5 - case forks.BPO4: - return c.BlobScheduleConfig.BPO4 - case forks.BPO3: - return c.BlobScheduleConfig.BPO3 - case forks.BPO2: - return c.BlobScheduleConfig.BPO2 - case forks.BPO1: - return c.BlobScheduleConfig.BPO1 - case forks.Osaka: - return c.BlobScheduleConfig.Osaka - case forks.Prague: - return c.BlobScheduleConfig.Prague - case forks.Cancun: - return c.BlobScheduleConfig.Cancun - default: + if c.BlobScheduleConfig == nil { return nil } + bsc := c.BlobScheduleConfig + chain := []struct { + at forks.Fork + cfg *BlobConfig + }{ + {forks.BPO5, bsc.BPO5}, + {forks.BPO4, bsc.BPO4}, + {forks.BPO3, bsc.BPO3}, + {forks.BPO2, bsc.BPO2}, + {forks.BPO1, bsc.BPO1}, + {forks.Prague, bsc.Prague}, + {forks.Cancun, bsc.Cancun}, + } + for _, e := range chain { + if e.at <= fork && e.cfg != nil { + return e.cfg + } + } + return nil } // ActiveSystemContracts returns the currently active system contracts at the @@ -1240,6 +1231,8 @@ func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { return c.CancunTime case fork == forks.Shanghai: return c.ShanghaiTime + case fork == forks.Amsterdam: + return c.AmsterdamTime default: return nil } diff --git a/params/protocol_params.go b/params/protocol_params.go index 84ded9df48..c4a74b794a 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -88,7 +88,6 @@ const ( LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction. Create2Gas uint64 = 32000 // Once per CREATE2 operation - CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately. CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation. MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL. @@ -101,7 +100,26 @@ const ( TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 - TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 + + RegularPerAuthBaseCost uint64 = 7816 // As defined by EIP-8037 and EIP-8038 + + // EIP-2780: resource-based intrinsic transaction gas. + TxBaseCost2780 uint64 = 12000 + ColdAccountAccess2780 uint64 = 3000 + CreateAccess2780 uint64 = 11000 + TxValueCost2780 uint64 = 4244 + TransferLogCost2780 uint64 = 1756 + + // EIP-8038: state-access gas cost update (Amsterdam). + ColdAccountAccessAmsterdam uint64 = 3000 // COLD_ACCOUNT_ACCESS: cold touch of an account + AccountWriteAmsterdam uint64 = 8000 // ACCOUNT_WRITE: surcharge for first-time write to an account + CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300) + ColdStorageAccessAmsterdam uint64 = 3000 // COLD_STORAGE_ACCESS: cold touch of a storage slot + StorageWriteAmsterdam uint64 = 10000 // STORAGE_WRITE: surcharge for first-time write to a storage slot + StorageClearRefundAmsterdam uint64 = 12480 // STORAGE_CLEAR_REFUND: refund for clearing a storage slot + CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS + TxAccessListAddressGasAmsterdam uint64 = 3000 // ACCESS_LIST_ADDRESS_COST + TxAccessListStorageKeyGasAmsterdam uint64 = 3000 // ACCESS_LIST_STORAGE_KEY_COST // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. @@ -246,6 +264,10 @@ var ( BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd") BuilderExitAddress = common.HexToAddress("0x000014574A74c805590AFF9499fc7A690f008282") BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd") + + // EIP-7997 - Deterministic deployment factory (keyless CREATE2 factory) + DeterministicFactoryAddress = common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C") + DeterministicFactoryCode = common.FromHex("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3") ) // System log events. diff --git a/rpc/types.go b/rpc/types.go index 578d3f86dd..7720a8d9c6 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -158,6 +158,9 @@ func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { if e.BlockNumber != nil && e.BlockHash != nil { return errors.New("cannot specify both BlockHash and BlockNumber, choose one or the other") } + if e.BlockNumber == nil && e.BlockHash == nil { + return errors.New("must specify either BlockHash or BlockNumber") + } bnh.BlockNumber = e.BlockNumber bnh.BlockHash = e.BlockHash bnh.RequireCanonical = e.RequireCanonical diff --git a/rpc/types_test.go b/rpc/types_test.go index 9dd6fa6508..35294c3dba 100644 --- a/rpc/types_test.go +++ b/rpc/types_test.go @@ -109,6 +109,8 @@ func TestBlockNumberOrHash_UnmarshalJSON(t *testing.T) { 27: {`{"blockNumber":"safe"}`, false, BlockNumberOrHashWithNumber(SafeBlockNumber)}, 28: {`{"blockNumber":"finalized"}`, false, BlockNumberOrHashWithNumber(FinalizedBlockNumber)}, 29: {`{"blockNumber":"0x1", "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, true, BlockNumberOrHash{}}, + 30: {`{}`, true, BlockNumberOrHash{}}, + 31: {`{"requireCanonical":true}`, true, BlockNumberOrHash{}}, } for i, test := range tests { diff --git a/tests/init.go b/tests/init.go index 2550eb1231..67f199203f 100644 --- a/tests/init.go +++ b/tests/init.go @@ -434,7 +434,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, "PragueToOsakaAtTime15k": { @@ -461,7 +460,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, "BPO1": { @@ -489,7 +487,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, }, }, @@ -518,7 +515,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, }, }, @@ -548,7 +544,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, }, @@ -579,7 +574,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, }, @@ -611,7 +605,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -644,7 +637,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -678,7 +670,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -713,7 +704,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -747,14 +737,12 @@ var Forks = map[string]*params.ChainConfig{ AmsterdamTime: u64(0), DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - Cancun: params.DefaultCancunBlobConfig, - Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, - BPO3: params.DefaultBPO3BlobConfig, - BPO4: params.DefaultBPO4BlobConfig, - Amsterdam: params.DefaultBPO4BlobConfig, // TODO update when defined + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + BPO1: bpo1BlobConfig, + BPO2: bpo2BlobConfig, + BPO3: params.DefaultBPO3BlobConfig, + BPO4: params.DefaultBPO4BlobConfig, }, }, "Verkle": { @@ -801,7 +789,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, } diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 91f7d6c3ec..010c31324b 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) // TransactionTest checks RLP decoding and sender derivation of transactions. @@ -81,7 +82,11 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic cost - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return sender, hash, 0, errors.New("value exceeds 256 bits") + } + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), sender, tx.To(), value, rules, params.CostPerStateByte) if err != nil { return } @@ -92,7 +97,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err = core.FloorDataGas(rules, sender, tx.To(), value, tx.Data(), tx.AccessList()) if err != nil { return } diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 7b05e68df9..e8b3021284 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -474,3 +474,32 @@ func (t *BinaryTrie) PrefetchStorage(addr common.Address, keys [][]byte) error { func (t *BinaryTrie) Witness() map[string][]byte { return t.tracer.Values() } + +// UpdateStorageBatch updates a list of storage slots sequentially. +func (t *BinaryTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error { + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + for i, key := range keys { + if err := t.UpdateStorage(address, key, values[i]); err != nil { + return err + } + } + return nil +} + +// UpdateAccountBatch updates a list of accounts sequentially. +func (t *BinaryTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error { + if len(addresses) != len(accounts) { + return fmt.Errorf("addresses and accounts length mismatch: %d != %d", len(addresses), len(accounts)) + } + if len(addresses) != len(codeLens) { + return fmt.Errorf("addresses and code length mismatch: %d != %d", len(addresses), len(codeLens)) + } + for i, addr := range addresses { + if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil { + return err + } + } + return nil +} diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 4d03ca45f0..9f6cea9790 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -210,6 +210,29 @@ func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error { return nil } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *StateTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error { + var ( + hkeys = make([][]byte, 0, len(keys)) + evals = make([][]byte, 0, len(values)) + ) + for _, key := range keys { + hk := crypto.Keccak256(key) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = common.CopyBytes(key) + } + hkeys = append(hkeys, hk) + } + for _, val := range values { + data, err := rlp.EncodeToBytes(val) + if err != nil { + return err + } + evals = append(evals, data) + } + return t.trie.UpdateBatch(hkeys, evals) +} + // UpdateAccount will abstract the write of an account to the secure trie. func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error { hk := crypto.Keccak256(address.Bytes()) @@ -226,6 +249,29 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun return nil } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *StateTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error { + var ( + hkeys = make([][]byte, 0, len(addresses)) + values = make([][]byte, 0, len(accounts)) + ) + for _, addr := range addresses { + hk := crypto.Keccak256(addr.Bytes()) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = addr.Bytes() + } + hkeys = append(hkeys, hk) + } + for _, acc := range accounts { + data, err := rlp.EncodeToBytes(acc) + if err != nil { + return err + } + values = append(values, data) + } + return t.trie.UpdateBatch(hkeys, values) +} + func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error { return nil } diff --git a/trie/tracer.go b/trie/tracer.go index 04122d1384..042fa468bf 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -33,12 +33,10 @@ import ( // while the latter is inserted/deleted in order to follow the rule of trie. // This tool can track all of them no matter the node is embedded in its // parent or not, but valueNode is never tracked. -// -// Note opTracer is not thread-safe, callers should be responsible for handling -// the concurrency issues by themselves. type opTracer struct { inserts map[string]struct{} deletes map[string]struct{} + lock sync.RWMutex } // newOpTracer initializes the tracer for capturing trie changes. @@ -53,6 +51,9 @@ func newOpTracer() *opTracer { // in the deletion set (resurrected node), then just wipe it from // the deletion set as it's "untouched". func (t *opTracer) onInsert(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.deletes[string(path)]; present { delete(t.deletes, string(path)) return @@ -64,6 +65,9 @@ func (t *opTracer) onInsert(path []byte) { // in the addition set, then just wipe it from the addition set // as it's untouched. func (t *opTracer) onDelete(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.inserts[string(path)]; present { delete(t.inserts, string(path)) return @@ -73,12 +77,18 @@ func (t *opTracer) onDelete(path []byte) { // reset clears the content tracked by tracer. func (t *opTracer) reset() { + t.lock.Lock() + defer t.lock.Unlock() + clear(t.inserts) clear(t.deletes) } // copy returns a deep copied tracer instance. func (t *opTracer) copy() *opTracer { + t.lock.RLock() + defer t.lock.RUnlock() + return &opTracer{ inserts: maps.Clone(t.inserts), deletes: maps.Clone(t.deletes), @@ -87,6 +97,9 @@ func (t *opTracer) copy() *opTracer { // deletedList returns a list of node paths which are deleted from the trie. func (t *opTracer) deletedList() [][]byte { + t.lock.RLock() + defer t.lock.RUnlock() + paths := make([][]byte, 0, len(t.deletes)) for path := range t.deletes { paths = append(paths, []byte(path)) diff --git a/trie/transitiontrie/transition.go b/trie/transitiontrie/transition.go index 3e5511be9e..d939e804e3 100644 --- a/trie/transitiontrie/transition.go +++ b/trie/transitiontrie/transition.go @@ -144,6 +144,19 @@ func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value return t.overlay.UpdateStorage(address, key, v) } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *TransitionTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error { + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + for i, key := range keys { + if err := t.UpdateStorage(address, key, values[i]); err != nil { + return err + } + } + return nil +} + // UpdateAccount abstract an account write to the trie. func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.StateAccount, codeLen int) error { // NOTE: before the rebase, this was saving the state root, so that OpenStorageTrie @@ -152,6 +165,22 @@ func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.State return t.overlay.UpdateAccount(addr, account, codeLen) } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *TransitionTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error { + if len(addresses) != len(accounts) { + return fmt.Errorf("address and accounts length mismatch: %d != %d", len(addresses), len(accounts)) + } + if len(addresses) != len(codeLens) { + return fmt.Errorf("address and code length mismatch: %d != %d", len(addresses), len(codeLens)) + } + for i, addr := range addresses { + if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil { + return err + } + } + return nil +} + // DeleteStorage removes any existing value for key from the trie. If a node was not // found in the database, a trie.MissingNodeError is returned. func (t *TransitionTrie) DeleteStorage(addr common.Address, key []byte) error { diff --git a/trie/trie.go b/trie/trie.go index 1ef2c2f1a6..0026c6048c 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -480,6 +480,95 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error } } +// parallelUpdateThreshold is the minimum number of entries a batch must contain +// before UpdateBatch processes them concurrently; smaller batches are applied +// sequentially to avoid the goroutine overhead. +const parallelUpdateThreshold = 4 + +// UpdateBatch updates a batch of entries concurrently. +func (t *Trie) UpdateBatch(keys [][]byte, values [][]byte) error { + // Short circuit if the trie is already committed and unusable. + if t.committed { + return ErrCommitted + } + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + // Insert the entries sequentially if there are not too many + // trie nodes in the trie. + fn, ok := t.root.(*fullNode) + if !ok || len(keys) < parallelUpdateThreshold { + return t.updateSequential(keys, values) + } + // Group the entries by the first nibble of the (hex) key, so that each group + // can be applied to a distinct child of the root independently. + var ( + ikeys = make(map[byte][][]byte) + ivals = make(map[byte][][]byte) + deleted [17]bool // child positions receiving at least one deletion + eg errgroup.Group + ) + for i, key := range keys { + hkey := keybytesToHex(key) + ikeys[hkey[0]] = append(ikeys[hkey[0]], hkey) + ivals[hkey[0]] = append(ivals[hkey[0]], values[i]) + if len(values[i]) == 0 { + deleted[hkey[0]] = true + } + } + // If the root may be collapsed after applying the updates, fallback + // to the sequential mode to eliminate the additional complexity. + survivors := 0 + for i, child := range &fn.Children { + if child != nil && !deleted[i] { + survivors++ + } + } + if survivors < 2 { + return t.updateSequential(keys, values) + } + // Execute the trie updates in concurrent mode + fn.flags = t.newFlag() + for pos, ks := range ikeys { + eg.Go(func() error { + vs := ivals[pos] + prefix := []byte{pos} + for i, k := range ks { + if len(vs[i]) != 0 { + _, n, err := t.insert(fn.Children[pos], prefix, k[1:], valueNode(vs[i])) + if err != nil { + return err + } + fn.Children[pos] = n + } else { + _, n, err := t.delete(fn.Children[pos], prefix, k[1:]) + if err != nil { + return err + } + fn.Children[pos] = n + } + } + return nil + }) + } + if err := eg.Wait(); err != nil { + return err + } + t.unhashed += len(keys) + t.uncommitted += len(keys) + return nil +} + +// updateSequential applies the given entries to the trie one by one. +func (t *Trie) updateSequential(keys [][]byte, values [][]byte) error { + for i, key := range keys { + if err := t.Update(key, values[i]); err != nil { + return err + } + } + return nil +} + // MustDelete is a wrapper of Delete and will omit any encountered error but // just print out an error message. func (t *Trie) MustDelete(key []byte) { diff --git a/trie/trie_test.go b/trie/trie_test.go index 3661933e22..949f381f07 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1580,3 +1580,57 @@ func BenchmarkTrieSeqPrefetch(b *testing.B) { } } } + +func TestUpdateBatch(t *testing.T) { + testUpdateBatch(t, []kv{ + {k: []byte("do"), v: []byte("verb")}, + {k: []byte("ether"), v: []byte("wookiedoo")}, + {k: []byte("horse"), v: []byte("stallion")}, + {k: []byte("shaman"), v: []byte("horse")}, + {k: []byte("doge"), v: []byte("coin")}, + {k: []byte("dog"), v: []byte("puppy")}, + }) + + var entries []kv + for i := 0; i < 256; i++ { + entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)}) + } + testUpdateBatch(t, entries) +} + +func testUpdateBatch(t *testing.T, entries []kv) { + var ( + base = NewEmpty(nil) + keys [][]byte + vals [][]byte + ) + for _, entry := range entries { + base.Update(entry.k, entry.v) + keys = append(keys, entry.k) + vals = append(vals, entry.v) + } + for i := 0; i < 10; i++ { + k, v := testrand.Bytes(32), testrand.Bytes(32) + base.Update(k, v) + keys = append(keys, k) + vals = append(vals, v) + } + + cmp := NewEmpty(nil) + if err := cmp.UpdateBatch(keys, vals); err != nil { + t.Fatalf("Failed to update batch, %v", err) + } + + // Traverse the original tree, the changes made on the copy one shouldn't + // affect the old one + for _, key := range keys { + v1, _ := base.Get(key) + v2, _ := cmp.Get(key) + if !bytes.Equal(v1, v2) { + t.Errorf("Unexpected data, key: %v, want: %v, got: %v", key, v1, v2) + } + } + if base.Hash() != cmp.Hash() { + t.Errorf("Hash mismatch: want %x, got %x", base.Hash(), cmp.Hash()) + } +} diff --git a/triedb/pathdb/nodes.go b/triedb/pathdb/nodes.go index 62c72c1953..590f7b499d 100644 --- a/triedb/pathdb/nodes.go +++ b/triedb/pathdb/nodes.go @@ -279,7 +279,7 @@ func (s *nodeSet) decode(r *rlp.Stream) error { // write flushes nodes into the provided database batch as a whole. func (s *nodeSet) write(batch ethdb.Batch, clean *fastcache.Cache) int { - nodes := make(map[common.Hash]map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string]*trienode.Node, len(s.storageNodes)+1) if len(s.accountNodes) > 0 { nodes[common.Hash{}] = s.accountNodes }