diff --git a/accounts/abi/argument.go b/accounts/abi/argument.go
index c1bf7aec86..83353c39f7 100644
--- a/accounts/abi/argument.go
+++ b/accounts/abi/argument.go
@@ -112,7 +112,7 @@ func (arguments Arguments) UnpackIntoMap(v map[string]any, data []byte) error {
// Copy performs the operation go format -> provided struct.
func (arguments Arguments) Copy(v any, values []any) error {
// make sure the passed value is arguments pointer
- if reflect.Ptr != reflect.ValueOf(v).Kind() {
+ if reflect.Pointer != reflect.ValueOf(v).Kind() {
return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
}
if len(values) == 0 {
diff --git a/accounts/abi/pack.go b/accounts/abi/pack.go
index a4c73922d4..5dead0f8ea 100644
--- a/accounts/abi/pack.go
+++ b/accounts/abi/pack.go
@@ -39,7 +39,7 @@ func packElement(t Type, reflectValue reflect.Value) ([]byte, error) {
switch t.T {
case UintTy:
// make sure to not pack a negative value into a uint type.
- if reflectValue.Kind() == reflect.Ptr {
+ if reflectValue.Kind() == reflect.Pointer {
val := new(big.Int).Set(reflectValue.Interface().(*big.Int))
if val.Sign() == -1 {
return nil, errInvalidSign
@@ -86,7 +86,7 @@ func packNum(value reflect.Value) []byte {
return math.U256Bytes(new(big.Int).SetUint64(value.Uint()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return math.U256Bytes(big.NewInt(value.Int()))
- case reflect.Ptr:
+ case reflect.Pointer:
return math.U256Bytes(new(big.Int).Set(value.Interface().(*big.Int)))
default:
panic("abi: fatal error")
diff --git a/accounts/abi/reflect.go b/accounts/abi/reflect.go
index f6696ea978..99c9cad767 100644
--- a/accounts/abi/reflect.go
+++ b/accounts/abi/reflect.go
@@ -53,7 +53,7 @@ func ConvertType(in interface{}, proto interface{}) interface{} {
// indirect recursively dereferences the value until it either gets the value
// or finds a big.Int
func indirect(v reflect.Value) reflect.Value {
- if v.Kind() == reflect.Ptr && v.Elem().Type() != reflect.TypeFor[big.Int]() {
+ if v.Kind() == reflect.Pointer && v.Elem().Type() != reflect.TypeFor[big.Int]() {
return indirect(v.Elem())
}
return v
@@ -102,9 +102,9 @@ func mustArrayToByteSlice(value reflect.Value) reflect.Value {
func set(dst, src reflect.Value) error {
dstType, srcType := dst.Type(), src.Type()
switch {
- case dstType.Kind() == reflect.Interface && dst.Elem().IsValid() && (dst.Elem().Type().Kind() == reflect.Ptr || dst.Elem().CanSet()):
+ case dstType.Kind() == reflect.Interface && dst.Elem().IsValid() && (dst.Elem().Type().Kind() == reflect.Pointer || dst.Elem().CanSet()):
return set(dst.Elem(), src)
- case dstType.Kind() == reflect.Ptr && dstType.Elem() != reflect.TypeFor[big.Int]():
+ case dstType.Kind() == reflect.Pointer && dstType.Elem() != reflect.TypeFor[big.Int]():
return set(dst.Elem(), src)
case srcType.AssignableTo(dstType) && dst.CanSet():
dst.Set(src)
@@ -138,7 +138,7 @@ func setSlice(dst, src reflect.Value) error {
}
func setArray(dst, src reflect.Value) error {
- if src.Kind() == reflect.Ptr {
+ if src.Kind() == reflect.Pointer {
return set(dst, indirect(src))
}
array := reflect.New(dst.Type()).Elem()
diff --git a/cmd/devp2p/internal/ethtest/transaction.go b/cmd/devp2p/internal/ethtest/transaction.go
index f9c2e304f3..66799bc44c 100644
--- a/cmd/devp2p/internal/ethtest/transaction.go
+++ b/cmd/devp2p/internal/ethtest/transaction.go
@@ -155,7 +155,11 @@ func (s *Suite) sendInvalidTxs(t *utesting.T, txs []*types.Transaction) error {
switch msg := msg.(type) {
case *eth.TransactionsPacket:
- for _, tx := range txs {
+ received, err := msg.Items()
+ if err != nil {
+ return fmt.Errorf("failed to decode received transactions: %w", err)
+ }
+ for _, tx := range received {
if _, ok := invalids[tx.Hash()]; ok {
return fmt.Errorf("received bad tx: %s", tx.Hash())
}
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 85a581eba6..f35b28510c 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -183,14 +183,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
blockAccessList = bal.NewConstructionBlockAccessList()
)
vmContext := vm.BlockContext{
- CanTransfer: core.CanTransfer,
- Transfer: core.Transfer,
- Coinbase: pre.Env.Coinbase,
- BlockNumber: new(big.Int).SetUint64(pre.Env.Number),
- Time: pre.Env.Timestamp,
- Difficulty: pre.Env.Difficulty,
- GasLimit: pre.Env.GasLimit,
- GetHash: getHash,
+ CanTransfer: core.CanTransfer,
+ Transfer: core.Transfer,
+ Coinbase: pre.Env.Coinbase,
+ BlockNumber: new(big.Int).SetUint64(pre.Env.Number),
+ Time: pre.Env.Timestamp,
+ Difficulty: pre.Env.Difficulty,
+ GasLimit: pre.Env.GasLimit,
+ GetHash: getHash,
+ CostPerStateByte: params.CostPerStateByte,
}
if pre.Env.SlotNumber != nil {
vmContext.SlotNum = *pre.Env.SlotNumber
diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go
index ca19ae3386..9eb1bdbf5f 100644
--- a/cmd/evm/internal/t8ntool/transaction.go
+++ b/cmd/evm/internal/t8ntool/transaction.go
@@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error {
}
// Check intrinsic gas
rules := chainConfig.Rules(common.Big0, true, 0)
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
r.Error = err
results = append(results, r)
diff --git a/cmd/geth/logging_test.go b/cmd/geth/logging_test.go
index d420c2d078..5131e6787b 100644
--- a/cmd/geth/logging_test.go
+++ b/cmd/geth/logging_test.go
@@ -96,7 +96,7 @@ func testConsoleLogging(t *testing.T, format string, tStart, tEnd int) {
}
}
if len(haveLines) != len(wantLines) {
- t.Errorf("format %v, want %d lines, have %d", format, len(haveLines), len(wantLines))
+ t.Errorf("format %v, want %d lines, have %d", format, len(wantLines), len(haveLines))
}
}
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 6bb54dc780..a248d4fa8a 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -58,6 +58,7 @@ import (
"github.com/ethereum/go-ethereum/graphql"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/flags"
+ "github.com/ethereum/go-ethereum/internal/memlimit"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/metrics/exp"
@@ -74,7 +75,6 @@ import (
"github.com/ethereum/go-ethereum/triedb/hashdb"
"github.com/ethereum/go-ethereum/triedb/pathdb"
pcsclite "github.com/gballet/go-libpcsclite"
- gopsutil "github.com/shirou/gopsutil/mem"
"github.com/urfave/cli/v2"
)
@@ -1726,16 +1726,18 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
setMiner(ctx, &cfg.Miner)
setRequiredBlocks(ctx, cfg)
- // Cap the cache allowance and tune the garbage collector
- mem, err := gopsutil.VirtualMemory()
- if err == nil {
- if 32<<(^uintptr(0)>>63) == 32 && mem.Total > 2*1024*1024*1024 {
- log.Warn("Lowering memory allowance on 32bit arch", "available", mem.Total/1024/1024, "addressable", 2*1024)
- mem.Total = 2 * 1024 * 1024 * 1024
+ // Cap the cache allowance and tune the garbage collector against
+ // the effective memory limit (cgroup-imposed when running in a
+ // container, total system memory otherwise).
+ total, source := memlimit.Limit()
+ if total > 0 {
+ if 32<<(^uintptr(0)>>63) == 32 && total > 2*1024*1024*1024 {
+ log.Warn("Lowering memory allowance on 32bit arch", "available", total/1024/1024, "addressable", 2*1024)
+ total = 2 * 1024 * 1024 * 1024
}
- allowance := int(mem.Total / 1024 / 1024 / 3)
+ allowance := int(total / 1024 / 1024 / 3)
if cache := ctx.Int(CacheFlag.Name); cache > allowance {
- log.Warn("Sanitizing cache to Go's GC limits", "provided", cache, "updated", allowance)
+ log.Warn("Sanitizing cache to Go's GC limits", "source", source, "provided", cache, "updated", allowance)
ctx.Set(CacheFlag.Name, strconv.Itoa(allowance))
}
}
@@ -1750,14 +1752,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
cfg.SyncMode = ethconfig.FullSync // dev sync target forces full sync
} else if ctx.IsSet(SyncModeFlag.Name) {
value := ctx.String(SyncModeFlag.Name)
- if err = cfg.SyncMode.UnmarshalText([]byte(value)); err != nil {
+ if err := cfg.SyncMode.UnmarshalText([]byte(value)); err != nil {
Fatalf("--%v: %v", SyncModeFlag.Name, err)
}
}
if ctx.IsSet(ChainHistoryFlag.Name) {
value := ctx.String(ChainHistoryFlag.Name)
- if err = cfg.HistoryMode.UnmarshalText([]byte(value)); err != nil {
+ if err := cfg.HistoryMode.UnmarshalText([]byte(value)); err != nil {
Fatalf("--%s: %v", ChainHistoryFlag.Name, err)
}
}
diff --git a/core/bal_test.go b/core/bal_test.go
index d20108b96c..b27901c083 100644
--- a/core/bal_test.go
+++ b/core/bal_test.go
@@ -153,6 +153,14 @@ func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
}
}
+// txGasNewAccount covers the base tx cost plus the EIP-8037 account-creation
+// state-gas charge (STATE_BYTES_PER_NEW_ACCOUNT × CPSB ≈ 183,600) that is
+// incurred when value is transferred to a non-existent account under Amsterdam.
+// params.TxGas (21,000) alone is insufficient: the transfer would run out of
+// gas, the credit would revert, and the recipient would never get a balance
+// change recorded in the BAL.
+const txGasNewAccount = 250_000
+
// --- tx builders ---
func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction {
@@ -177,7 +185,7 @@ func TestBALTxSenderAndRecipient(t *testing.T) {
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil))
+ g.AddTx(env.tx(0, &to, big.NewInt(1000), txGasNewAccount, 0, nil))
})
sender := assertPresent(t, b, env.from)
@@ -260,7 +268,7 @@ func TestBALSystemAddressIncludedWhenTouched(t *testing.T) {
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil))
+ g.AddTx(env.tx(0, &sys, big.NewInt(1000), txGasNewAccount, 0, nil))
})
aa := assertPresent(t, b, sys)
@@ -283,7 +291,7 @@ func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, identity)
@@ -315,7 +323,7 @@ func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) {
env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil))
+ g.AddTx(env.tx(0, &identity, big.NewInt(5), txGasNewAccount, 0, nil))
})
aa := assertPresent(t, b, identity)
@@ -334,7 +342,7 @@ func TestBALBalanceProbeOnNonExistent(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
@@ -350,7 +358,7 @@ func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
@@ -366,7 +374,7 @@ func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
@@ -385,7 +393,7 @@ func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, probe))
@@ -447,7 +455,7 @@ func TestBALCallTargetWithEmptyChangeSet(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &target, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, target))
@@ -465,7 +473,7 @@ func TestBALCallCodeTargetIncluded(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertPresent(t, b, caller)
@@ -483,7 +491,7 @@ func TestBALDelegateCallTargetIncluded(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertPresent(t, b, caller)
@@ -501,7 +509,7 @@ func TestBALStaticCallTargetIncluded(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
})
assertPresent(t, b, caller)
@@ -519,7 +527,7 @@ func TestBALRevertedTxStillIncluded(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 1_000_000, 0, nil))
})
assertEmpty(t, assertPresent(t, b, reverter))
@@ -533,7 +541,7 @@ func TestBALSenderRecordedOnRevert(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &reverter, big.NewInt(0), 1_000_000, 0, nil))
})
sender := assertPresent(t, b, env.from)
@@ -557,7 +565,7 @@ func TestBALStorageWriteRecorded(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
@@ -578,7 +586,7 @@ func TestBALStorageSloadOnly(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
@@ -604,7 +612,7 @@ func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
@@ -632,7 +640,7 @@ func TestBALNoOpSSTOREDemotesToRead(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
@@ -660,7 +668,7 @@ func TestBALStorageWriteZeroIsAWrite(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil))
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, contract)
@@ -687,7 +695,7 @@ func TestBALCreateDeploysCode(t *testing.T) {
init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(7), 1_000_000, 0, init))
})
created := receipts[0].ContractAddress
@@ -711,7 +719,7 @@ func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) {
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init))
})
created := receipts[0].ContractAddress
@@ -732,7 +740,7 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
b, receipts := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init))
})
created := receipts[0].ContractAddress
@@ -743,11 +751,13 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
// the deployed address in the BAL with an empty change set.
func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) {
env := newBALTestEnv(nil)
- // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG.
+ // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. The
+ // gas budget must cover EIP-8037 intrinsic state gas (account creation)
+ // so the tx is accepted; OOG must happen inside the init code.
init := []byte{0x5b, 0x60, 0x00, 0x56}
b, receipts := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 220_000, 0, init))
})
created := receipts[0].ContractAddress
@@ -771,7 +781,7 @@ func TestBALCreateAddressCollisionStillIncluded(t *testing.T) {
// Init code doesn't matter — execution never starts.
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init))
})
aa := assertPresent(t, b, collide)
@@ -799,7 +809,7 @@ func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 1_000_000, 0, nil))
})
// The address that WOULD have been deployed had the create succeeded.
@@ -842,7 +852,7 @@ func TestBALInEVMCreateDeploysContract(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil))
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 1_000_000, 0, nil))
})
// Deployed address depends on the factory's nonce at the moment of CREATE,
@@ -870,7 +880,7 @@ func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) {
init = append(init, 0xff)
b, receipts := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init))
})
created := receipts[0].ContractAddress
@@ -896,7 +906,7 @@ func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) {
init = append(init, 0xff)
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init))
+ g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init))
})
ben := assertPresent(t, b, beneficiary)
@@ -921,7 +931,7 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) {
})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, suicidal)
@@ -966,7 +976,7 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil))
+ g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 1_000_000, 0, nil))
})
aa := assertPresent(t, b, bouncer)
@@ -1072,7 +1082,7 @@ func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) {
Nonce: 0,
To: env.from,
Value: new(uint256.Int),
- Gas: 200_000,
+ Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
@@ -1112,7 +1122,7 @@ func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) {
Nonce: 0,
To: env.from, // tx.to is an EOA with no code: delegate is never called
Value: new(uint256.Int),
- Gas: 200_000,
+ Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth},
@@ -1131,7 +1141,7 @@ func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address,
Nonce: nonce,
To: to,
Value: new(uint256.Int),
- Gas: 400_000,
+ Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int),
AuthList: auths,
diff --git a/core/bench_test.go b/core/bench_test.go
index 65179c54d4..fe66aeae0d 100644
--- a/core/bench_test.go
+++ b/core/bench_test.go
@@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
data := make([]byte, nbytes)
return func(i int, gen *BlockGen) {
toaddr := common.Address{}
- cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false)
+ cost, _ := IntrinsicGas(data, nil, nil, false, 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 b49ac83bb5..e151a72801 100644
--- a/core/bintrie_witness_test.go
+++ b/core/bintrie_witness_test.go
@@ -64,12 +64,12 @@ var (
func TestProcessUBT(t *testing.T) {
var (
code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`)
- intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false)
+ intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, 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, true, true, true, false)
+ intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0)
signer = types.LatestSigner(testUBTChainConfig)
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain
@@ -201,7 +201,7 @@ func TestProcessParentBlockHash(t *testing.T) {
if isUBT {
chainConfig = testUBTChainConfig
}
- vmContext := NewEVMBlockContext(header, nil, new(common.Address))
+ vmContext := NewEVMBlockContext(header, &BlockChain{chainConfig: chainConfig}, new(common.Address))
evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList())
}
diff --git a/core/blockchain.go b/core/blockchain.go
index c914a6dd81..ecb33ce0b8 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -1194,10 +1194,13 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash, isSnapV2 bool) error {
return fmt.Errorf("non existent state [%x..]", root[:4])
}
- // The legacy snapshot tree needs to be wiped and rebuilt from the trie
- // after a snap/1 sync.
- if !isSnapV2 && bc.snaps != nil {
- bc.snaps.Rebuild(root)
+ // The legacy snapshot tree (hash scheme only) was persistently disabled
+ // before the sync, re-enables it explicitly.
+ //
+ // For snap/2 the downloaded flat state is already complete and root-verified,
+ // so the background generation is unnecessary.
+ if bc.snaps != nil {
+ bc.snaps.Rebuild(root, !isSnapV2)
}
// If all checks out, manually set the head block.
@@ -2316,6 +2319,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
stats.CrossValidation = xvtime // The time spent on stateless cross validation
+ // Attach the computed block access list so it gets persisted alongside the
+ // block. The validator has already verified the hash matches the header.
+ // BAL is only meaningful from Amsterdam onward; skip pre-Amsterdam blocks
+ // to avoid persisting and serving empty BALs over the network.
+ if res.Bal != nil && block.AccessList() == nil && bc.chainConfig.IsAmsterdam(block.Number(), block.Time()) {
+ block = block.WithAccessListUnsafe(res.Bal.ToEncodingObj())
+ }
+
// Write the block to the chain and get the status.
var status WriteStatus
if config.WriteState {
diff --git a/core/blockchain_snapshot_test.go b/core/blockchain_snapshot_test.go
index ae9398b97d..cd95f19697 100644
--- a/core/blockchain_snapshot_test.go
+++ b/core/blockchain_snapshot_test.go
@@ -29,6 +29,7 @@ import (
"testing"
"time"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb"
@@ -721,3 +722,40 @@ func TestRecoverSnapshotFromWipingCrash(t *testing.T) {
test.teardown()
}
}
+
+// TestSnapSyncCompleteRebuildsSnapshot verifies that completing a snap sync
+// re-enables the legacy snapshot tree on the hash scheme for both syncer
+// versions: SnapSyncStart persistently disables the tree, and only the
+// rebuild on completion clears the marker again.
+func TestSnapSyncCompleteRebuildsSnapshot(t *testing.T) {
+ for _, isSnapV2 := range []bool{false, true} {
+ _, _, chain, err := newCanonical(ethash.NewFaker(), 8, true, rawdb.HashScheme)
+ if err != nil {
+ t.Fatalf("failed to create chain: %v", err)
+ }
+ if err := chain.SnapSyncStart(); err != nil {
+ t.Fatalf("failed to start snap sync: %v", err)
+ }
+ if !rawdb.ReadSnapshotDisabled(chain.db) {
+ t.Fatal("snapshot should be disabled during snap sync")
+ }
+ head := chain.CurrentBlock()
+ if err := chain.SnapSyncComplete(head.Hash(), isSnapV2); err != nil {
+ t.Fatalf("failed to complete snap sync (v2=%v): %v", isSnapV2, err)
+ }
+ if rawdb.ReadSnapshotDisabled(chain.db) {
+ t.Fatalf("snapshot should be re-enabled after snap sync completion (v2=%v)", isSnapV2)
+ }
+ // snap/2 adopts the flat state without regeneration, so the snapshot
+ // must be immediately usable; snap/1 schedules a background rebuild
+ // instead (which may or may not have finished, no assertion there).
+ if isSnapV2 {
+ it, err := chain.snaps.AccountIterator(head.Root, common.Hash{})
+ if err != nil {
+ t.Fatalf("adopted snapshot not immediately usable: %v", err)
+ }
+ it.Release()
+ }
+ chain.Stop()
+ }
+}
diff --git a/core/evm.go b/core/evm.go
index 73e4c01a99..fdea63e469 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -68,18 +68,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
}
return vm.BlockContext{
- CanTransfer: CanTransfer,
- Transfer: Transfer,
- GetHash: GetHashFn(header, chain),
- Coinbase: beneficiary,
- BlockNumber: new(big.Int).Set(header.Number),
- Time: header.Time,
- Difficulty: new(big.Int).Set(header.Difficulty),
- BaseFee: baseFee,
- BlobBaseFee: blobBaseFee,
- GasLimit: header.GasLimit,
- Random: random,
- SlotNum: slotNum,
+ CanTransfer: CanTransfer,
+ Transfer: Transfer,
+ GetHash: GetHashFn(header, chain),
+ Coinbase: beneficiary,
+ BlockNumber: new(big.Int).Set(header.Number),
+ Time: header.Time,
+ Difficulty: new(big.Int).Set(header.Difficulty),
+ BaseFee: baseFee,
+ BlobBaseFee: blobBaseFee,
+ GasLimit: header.GasLimit,
+ Random: random,
+ SlotNum: slotNum,
+ CostPerStateByte: params.CostPerStateByte,
}
}
diff --git a/core/gaspool.go b/core/gaspool.go
index 14f5abd93c..83420c7640 100644
--- a/core/gaspool.go
+++ b/core/gaspool.go
@@ -27,6 +27,10 @@ type GasPool struct {
remaining uint64
initial uint64
cumulativeUsed uint64
+
+ // After 8037 Block gas used is max(cumulativeRegular, cumulativeState).
+ cumulativeRegular uint64
+ cumulativeState uint64
}
// NewGasPool initializes the gasPool with the given amount.
@@ -37,9 +41,9 @@ func NewGasPool(amount uint64) *GasPool {
}
}
-// SubGas deducts the given amount from the pool if enough gas is
+// CheckGasLegacy deducts the given amount from the pool if enough gas is
// available and returns an error otherwise.
-func (gp *GasPool) SubGas(amount uint64) error {
+func (gp *GasPool) CheckGasLegacy(amount uint64) error {
if gp.remaining < amount {
return ErrGasLimitReached
}
@@ -47,41 +51,73 @@ func (gp *GasPool) SubGas(amount uint64) error {
return nil
}
-// ReturnGas adds the refunded gas back to the pool and updates
+// CheckGasAmsterdam performs the EIP-8037 per-tx 2D block-inclusion check:
+// the worst-case regular contribution must fit in the regular dimension and
+// the worst-case state contribution must fit in the state dimension
+func (gp *GasPool) CheckGasAmsterdam(regularReservation, stateReservation uint64) error {
+ if gp.initial-gp.cumulativeRegular < regularReservation {
+ return ErrGasLimitReached
+ }
+ if gp.initial-gp.cumulativeState < stateReservation {
+ return ErrGasLimitReached
+ }
+ return nil
+}
+
+// ChargeGasLegacy adds the refunded gas back to the pool and updates
// the cumulative gas usage accordingly.
-func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error {
+func (gp *GasPool) ChargeGasLegacy(returned uint64, gasUsed uint64) error {
if gp.remaining > math.MaxUint64-returned {
return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned)
}
- // The returned gas calculation differs across forks.
- //
- // - Pre-Amsterdam:
- // returned = purchased - remaining (refund included)
- //
- // - Post-Amsterdam:
- // returned = purchased - gasUsed (refund excluded)
+ // returned = purchased - remaining (refund included)
gp.remaining += returned
// gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost)
- // regardless of Amsterdam is activated or not.
gp.cumulativeUsed += gasUsed
return nil
}
+// ChargeGasAmsterdam calculates the new remaining gas in the pool after the
+// execution of a message. Previously we subtracted and re-added gas to the
+// gaspool. After Amsterdam we only check if we can include the transaction
+// and charge the gaspool at the end.
+func (gp *GasPool) ChargeGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error {
+ cumulativeRegular := gp.cumulativeRegular + txRegular
+ cumulativeState := gp.cumulativeState + txState
+ blockUsed := max(cumulativeRegular, cumulativeState)
+ if gp.initial < blockUsed {
+ return fmt.Errorf("%w: block gas overflow: initial %d, used %d (regular: %d, state: %d)",
+ ErrGasLimitReached, gp.initial, blockUsed, cumulativeRegular, cumulativeState)
+ }
+ gp.cumulativeRegular = cumulativeRegular
+ gp.cumulativeState = cumulativeState
+ gp.cumulativeUsed += receiptGasUsed
+ // TODO(rjl, marius), the semantics of this counter is slightly different
+ // in the context of Amsterdam, the API Gas() should be reworked.
+ gp.remaining = gp.initial - gp.cumulativeRegular
+ return nil
+}
+
// Gas returns the amount of gas remaining in the pool.
func (gp *GasPool) Gas() uint64 {
return gp.remaining
}
-// CumulativeUsed returns the amount of cumulative consumed gas (refunded included).
+// CumulativeUsed returns the cumulative gas consumed for receipt tracking.
func (gp *GasPool) CumulativeUsed() uint64 {
return gp.cumulativeUsed
}
// Used returns the amount of consumed gas.
func (gp *GasPool) Used() uint64 {
+ // After 8037, return max(sum_regular, sum_state)
+ if gp.cumulativeRegular > 0 || gp.cumulativeState > 0 {
+ return max(gp.cumulativeRegular, gp.cumulativeState)
+ }
+ // Before 8037, return initial-remaining
if gp.initial < gp.remaining {
- panic("gas used underflow")
+ panic(fmt.Sprintf("gas used underflow: %v %v", gp.initial, gp.remaining))
}
return gp.initial - gp.remaining
}
@@ -89,9 +125,11 @@ func (gp *GasPool) Used() uint64 {
// Snapshot returns the deep-copied object as the snapshot.
func (gp *GasPool) Snapshot() *GasPool {
return &GasPool{
- initial: gp.initial,
- remaining: gp.remaining,
- cumulativeUsed: gp.cumulativeUsed,
+ initial: gp.initial,
+ remaining: gp.remaining,
+ cumulativeUsed: gp.cumulativeUsed,
+ cumulativeRegular: gp.cumulativeRegular,
+ cumulativeState: gp.cumulativeState,
}
}
@@ -100,6 +138,8 @@ func (gp *GasPool) Set(other *GasPool) {
gp.initial = other.initial
gp.remaining = other.remaining
gp.cumulativeUsed = other.cumulativeUsed
+ gp.cumulativeRegular = other.cumulativeRegular
+ gp.cumulativeState = other.cumulativeState
}
func (gp *GasPool) String() string {
diff --git a/core/rawdb/accessors_snapshot.go b/core/rawdb/accessors_snapshot.go
index 4573e08321..8872b00fc2 100644
--- a/core/rawdb/accessors_snapshot.go
+++ b/core/rawdb/accessors_snapshot.go
@@ -209,48 +209,6 @@ func WriteSnapshotSyncStatus(db ethdb.KeyValueWriter, status []byte) {
}
}
-// ReadGenerateTriePartitionDone returns the raw subtree root blob for a
-// partition that has previously completed.
-func ReadGenerateTriePartitionDone(db ethdb.KeyValueReader, partition byte) ([]byte, bool) {
- data, err := db.Get(generateTriePartitionDoneKey(partition))
- if err != nil {
- return nil, false
- }
- if len(data) == 0 {
- return nil, false
- }
- switch data[0] {
- case 0x00:
- // Partition is done and it is empty.
- return nil, true
- case 0x01:
- // Partition is done and the blob follows.
- return data[1:], true
- default:
- return nil, false
- }
-}
-
-// WriteGenerateTriePartitionDone records a completed partition.
-func WriteGenerateTriePartitionDone(db ethdb.KeyValueWriter, partition byte, blob []byte) {
- var value []byte
- if blob == nil {
- value = []byte{0x00}
- } else {
- value = append([]byte{0x01}, blob...)
- }
- if err := db.Put(generateTriePartitionDoneKey(partition), value); err != nil {
- log.Crit("Failed to store generate-trie done marker", "err", err)
- }
-}
-
-// DeleteGenerateTriePartitionDone removes a partition's done marker.
-func DeleteGenerateTriePartitionDone(db ethdb.KeyValueWriter, partition byte) {
- if err := db.Delete(generateTriePartitionDoneKey(partition)); err != nil {
- log.Crit("Failed to remove generate-trie done marker", "err", err)
- }
-}
-
// DeleteSnapshotSyncStatus removes the serialized sync status from the database.
func DeleteSnapshotSyncStatus(db ethdb.KeyValueWriter) {
if err := db.Delete(snapshotSyncStatusKey); err != nil {
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index bf19f41c44..8063bc6419 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -563,8 +563,6 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
}
// Metadata keys
- case bytes.HasPrefix(key, generateTriePartitionDonePrefix) && len(key) == len(generateTriePartitionDonePrefix)+1:
- metadata.add(size)
case slices.ContainsFunc(knownMetadataKeys, func(x []byte) bool { return bytes.Equal(x, key) }):
metadata.add(size)
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index da5f9608bf..54c76143b4 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -104,10 +104,6 @@ var (
// snapSyncStatusFlagKey flags that status of snap sync.
snapSyncStatusFlagKey = []byte("SnapSyncStatus")
- // generateTriePartitionDonePrefix stores the subtree root hash of each
- // triedb.GenerateTrie partition once it finishes.
- generateTriePartitionDonePrefix = []byte("gtd") // generateTriePartitionDonePrefix + partition byte -> subtree root hash
-
// Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes).
headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td (deprecated)
@@ -469,8 +465,3 @@ func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID
func transitionStateKey(hash common.Hash) []byte {
return append(VerkleTransitionStatePrefix, hash.Bytes()...)
}
-
-// generateTriePartitionDoneKey = generateTriePartitionDonePrefix + partition (single byte).
-func generateTriePartitionDoneKey(partition byte) []byte {
- return append(generateTriePartitionDonePrefix, partition)
-}
diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go
index cd0a55fee6..d3a5e9aa21 100644
--- a/core/state/snapshot/snapshot.go
+++ b/core/state/snapshot/snapshot.go
@@ -23,6 +23,7 @@ import (
"fmt"
"sync"
+ "github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
@@ -213,7 +214,7 @@ func New(config Config, diskdb ethdb.KeyValueStore, triedb *triedb.Database, roo
if err != nil {
log.Warn("Failed to load snapshot", "err", err)
if !config.NoBuild {
- snap.Rebuild(root)
+ snap.Rebuild(root, true)
return snap, nil
}
return nil, err // Bail out the error, don't rebuild automatically.
@@ -683,10 +684,12 @@ func (t *Tree) Journal(root common.Hash) (common.Hash, error) {
return base, nil
}
-// Rebuild wipes all available snapshot data from the persistent database and
-// discard all caches and diff layers. Afterwards, it starts a new snapshot
-// generator with the given root hash.
-func (t *Tree) Rebuild(root common.Hash) {
+// Rebuild discards all caches and diff layers and re-enables the snapshot
+// feature. With generate set, it starts a new snapshot generator with the
+// given root hash, wiping and regenerating the persistent flat state in the
+// background. Without it, the on-disk flat state is adopted as the fully
+// generated disk layer directly.
+func (t *Tree) Rebuild(root common.Hash, generate bool) {
t.lock.Lock()
defer t.lock.Unlock()
@@ -715,6 +718,26 @@ func (t *Tree) Rebuild(root common.Hash) {
panic(fmt.Sprintf("unknown layer type: %T", layer))
}
}
+ // Adopt the existing flat state as the generated disk layer if
+ // regeneration was not requested.
+ if !generate {
+ batch := t.diskdb.NewBatch()
+ rawdb.WriteSnapshotRoot(batch, root)
+ journalProgress(batch, nil, nil)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to write snapshot completion marker", "err", err)
+ }
+ log.Info("Adopted state snapshot", "root", root)
+ t.layers = map[common.Hash]snapshot{
+ root: &diskLayer{
+ diskdb: t.diskdb,
+ triedb: t.triedb,
+ cache: fastcache.New(t.config.CacheSize * 1024 * 1024),
+ root: root,
+ },
+ }
+ return
+ }
// Start generating a new snapshot from scratch on a background thread. The
// generator will run a wiper first if there's not one running right now.
log.Info("Rebuilding state snapshot")
diff --git a/core/state_processor.go b/core/state_processor.go
index 5f43206eb4..f40aee0301 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -271,6 +271,21 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *
return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm)
}
+// systemCallGasBudget returns the gas budget for system calls.
+func systemCallGasBudget(evm *vm.EVM) (gasLimit uint64, gasBudget vm.GasBudget) {
+ if !evm.GetRules().IsAmsterdam {
+ gasLimit = 30_000_000
+ gasBudget = vm.NewGasBudget(gasLimit, 0)
+ } else {
+ // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of
+ // new storage slots a single system call is expected to write.
+ stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize
+ gasLimit = 30_000_000
+ gasBudget = vm.NewGasBudget(gasLimit, stateBudget)
+ }
+ return gasLimit, gasBudget
+}
+
// ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root
// contract. This method is exported to be used in tests.
func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) {
@@ -280,9 +295,10 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList
defer tracer.OnSystemCallEnd()
}
}
+ gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: gasLimit,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -293,7 +309,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList
evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
- _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
@@ -309,9 +325,10 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *
defer tracer.OnSystemCallEnd()
}
}
+ gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: gasLimit,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -322,7 +339,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *
evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress)
- _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ _, _, err := evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560)
if err != nil {
panic(err)
}
@@ -351,9 +368,10 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E
defer tracer.OnSystemCallEnd()
}
}
+ gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{
From: params.SystemAddress,
- GasLimit: 30_000_000,
+ GasLimit: gasLimit,
GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0),
GasTipCap: uint256.NewInt(0),
@@ -363,7 +381,7 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E
evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr)
- ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560)
+ ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
diff --git a/core/state_transition.go b/core/state_transition.go
index 724b9963f8..dac8123530 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -68,13 +68,27 @@ func (result *ExecutionResult) Revert() []byte {
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
-func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) {
+func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) {
// Set the starting gas for the raw transaction
- var gas uint64
- if isContractCreation && isHomestead {
- gas = params.TxGasContractCreation
+ var gas vm.GasCosts
+ if isContractCreation && rules.IsHomestead {
+ if rules.IsAmsterdam {
+ gas.RegularGas = params.TxGas + params.CreateGasAmsterdam
+ gas.StateGas = params.AccountCreationSize * costPerStateByte
+ } else {
+ gas.RegularGas = params.TxGasContractCreation
+ }
} else {
- gas = params.TxGas
+ gas.RegularGas = params.TxGas
+ }
+ // Add gas for authorizations
+ if authList != nil {
+ if rules.IsAmsterdam {
+ gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas
+ gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte
+ } else {
+ gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas
+ }
}
dataLen := uint64(len(data))
// Bump the required gas by the amount of transactional data
@@ -85,59 +99,56 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
// Make sure we don't exceed uint64 for all data combinations
nonZeroGas := params.TxDataNonZeroGasFrontier
- if isEIP2028 {
+ if rules.IsIstanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
- if (math.MaxUint64-gas)/nonZeroGas < nz {
+ if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += nz * nonZeroGas
+ gas.RegularGas += nz * nonZeroGas
- if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
+ if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += z * params.TxDataZeroGas
+ gas.RegularGas += z * params.TxDataZeroGas
- if isContractCreation && isEIP3860 {
+ if isContractCreation && rules.IsShanghai {
lenWords := toWordSize(dataLen)
- if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords {
+ if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += lenWords * params.InitCodeWordGas
+ gas.RegularGas += lenWords * params.InitCodeWordGas
}
}
if accessList != nil {
addresses := uint64(len(accessList))
storageKeys := uint64(accessList.StorageKeys())
- if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses {
+ if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += addresses * params.TxAccessListAddressGas
- if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys {
+ gas.RegularGas += addresses * params.TxAccessListAddressGas
+ if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += storageKeys * params.TxAccessListStorageKeyGas
+ gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas
// EIP-7981: access list data is charged in addition to the base charge.
- if isAmsterdam {
+ if rules.IsAmsterdam {
const (
addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
)
- if (math.MaxUint64-gas)/addressCost < addresses {
+ if (math.MaxUint64-gas.RegularGas)/addressCost < addresses {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += addresses * addressCost
- if (math.MaxUint64-gas)/storageKeyCost < storageKeys {
+ gas.RegularGas += addresses * addressCost
+ if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas += storageKeys * storageKeyCost
+ gas.RegularGas += storageKeys * storageKeyCost
}
}
- if authList != nil {
- gas += uint64(len(authList)) * params.CallNewAccountGas
- }
- return vm.GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
@@ -338,12 +349,11 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
// 5. Run Script section
// 6. Derive new state root
type stateTransition struct {
- gp *GasPool
- msg *Message
- initialBudget vm.GasBudget
- gasRemaining vm.GasBudget
- state vm.StateDB
- evm *vm.EVM
+ gp *GasPool
+ msg *Message
+ gasRemaining vm.GasBudget
+ state vm.StateDB
+ evm *vm.EVM
}
// newStateTransition initialises and returns a new state transition object.
@@ -364,6 +374,24 @@ func (st *stateTransition) to() common.Address {
return *st.msg.To
}
+// buyGas pre-pays gas from the sender's balance and initializes the
+// transaction's gas budget. It is invoked at the tail of preCheck.
+//
+// The balance requirement is the worst-case ETH the tx may need to lock
+// up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`,
+// plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance
+// returns ErrInsufficientFunds. After the check, the sender is actually
+// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee`
+// under Cancun), the cap-vs-tip differential is settled at tx end.
+//
+// The gas budget is seeded into both `initialBudget` (frozen snapshot
+// for tx-end accounting) and `gasRemaining` (live running balance):
+//
+// - Pre-Amsterdam: one-dimensional regular budget equal to
+// `msg.GasLimit`; the state-gas reservoir is zero.
+// - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is
+// capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from
+// `msg.GasLimit` above that cap becomes the state-gas reservoir.
func (st *stateTransition) buyGas() error {
mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
_, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice)
@@ -416,22 +444,53 @@ func (st *stateTransition) buyGas() error {
if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
}
- if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
+ isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
+
+ // Reserve the gas budget in the block gas pool
+ var err error
+ if isAmsterdam {
+ err = st.gp.CheckGasAmsterdam(min(st.msg.GasLimit, params.MaxTxGas), st.msg.GasLimit)
+ } else {
+ err = st.gp.CheckGasLegacy(st.msg.GasLimit)
+ }
+ if err != nil {
return err
}
- if st.evm.Config.Tracer.HasGasHook() {
- empty := vm.GasBudget{}
- initial := vm.NewGasBudget(st.msg.GasLimit)
- st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance)
+ // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit
+ limit := st.msg.GasLimit
+ if isAmsterdam {
+ limit = min(st.msg.GasLimit, params.MaxTxGas)
}
- st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit)
- st.initialBudget = st.gasRemaining.Copy()
+ st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit)
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance)
+ }
+ // Deduct the gas cost from the sender's balance
st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy)
return nil
}
+// preCheck performs all pre-execution validation that does not require
+// the EVM to run, then ends by calling buyGas to lock in the gas budget.
+// It returns a consensus error if any of the following fail:
+//
+// - Sender nonce matches state and is not at 2^64-1 (EIP-2681).
+// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam
+// (the cap also bounds the regular dimension after Amsterdam, but
+// it is enforced there via the two-dimensional budget in buyGas).
+// - EIP-3607 sender-is-EOA, allowing accounts whose only code is an
+// EIP-7702 delegation designator.
+// - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+).
+// - Blob-tx structural checks: non-nil `To`, non-empty hash list,
+// valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+).
+// - Blob fee-cap not below the current blob base fee (Cancun+).
+// - EIP-7702 set-code-tx shape: non-nil `To` and non-empty
+// authorization list.
+//
+// The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass
+// subsets of these checks for simulation paths (eth_call, eth_estimateGas).
func (st *stateTransition) preCheck() error {
// Only check transactions that are not fake
msg := st.msg
@@ -449,11 +508,13 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce)
}
}
- isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
- isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ var (
+ isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time)
+ )
if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap.
- if isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas {
+ if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
// Make sure the sender is an EOA
@@ -527,40 +588,33 @@ func (st *stateTransition) preCheck() error {
return st.buyGas()
}
-// execute will transition the state by applying the current message and
-// returning the evm execution result with following fields.
+// execute transitions the state by applying the current message and
+// returns the EVM execution result with the following fields:
//
-// - used gas: total gas used (including gas being refunded)
-// - returndata: the returned data from evm
-// - concrete execution error: various EVM errors which abort the execution, e.g.
-// ErrOutOfGas, ErrExecutionReverted
+// - used gas: total gas used, including gas refunded
+// - peak used gas: maximum gas used before applying refunds
+// - returndata: data returned by the EVM
+// - execution error: EVM-level errors that abort execution, such as
+// ErrOutOfGas or ErrExecutionReverted
//
-// However if any consensus issue encountered, return the error directly with
-// nil evm execution result.
+// If a consensus error is encountered, it is returned directly with a
+// nil EVM execution result.
func (st *stateTransition) execute() (*ExecutionResult, error) {
- // First check this message satisfies all consensus rules before
- // applying the message. The rules include these clauses
- //
- // 1. the nonce of the message caller is correct
- // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice)
- // 3. the amount of gas required is available in the block
- // 4. the purchased gas is enough to cover intrinsic usage
- // 5. there is no overflow when calculating intrinsic gas
- // 6. caller has enough balance to cover asset transfer for **topmost** call
-
- // Check clauses 1-3, buy gas if everything is correct
+ // Validate the message and pre-pay gas.
if err := st.preCheck(); err != nil {
return nil, err
}
+ // Charge intrinsic gas (with overflow detection inside IntrinsicGas).
+ // Under Amsterdam the cost is two-dimensional and Charge debits both
+ // regular and state in one step.
var (
msg = st.msg
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
contractCreation = msg.To == nil
floorDataGas uint64
)
- // Check clauses 4-5, subtract intrinsic gas if everything is correct
- cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte)
if err != nil {
return nil, err
}
@@ -571,15 +625,24 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas)
}
- // Gas limit suffices for the floor data cost (EIP-7623)
+
+ // 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)
if err != nil {
return nil, err
}
+ // Make sure the transaction has sufficient gas allowance to
+ // pay the floor cost.
if msg.GasLimit < floorDataGas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas)
}
+ // In Amsterdam, the transaction gas limit is allowed to exceed
+ // params.MaxTxGas, but the calldata floor cost is capped by it.
+ if rules.IsAmsterdam && max(cost.RegularGas, floorDataGas) > params.MaxTxGas {
+ return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas)
+ }
}
if rules.IsEIP4762 {
@@ -590,7 +653,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
}
- // Check clause 6
+ // Top-call affordability, the sender must still be able to cover the value
+ // transfer of the top frame after gas pre-pay.
value := msg.Value
if value == nil {
value = new(uint256.Int)
@@ -599,36 +663,43 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
}
- // Check whether the init code size has been exceeded.
- if contractCreation {
- if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil {
- return nil, err
- }
- }
-
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(EIP-1153)
// - enable block-level accessList construction (EIP-7928)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
+ // Execute the top-most frame
var (
- ret []byte
- vmerr error // vm errors do not effect consensus and are therefore not assigned to err
+ ret []byte
+ vmerr error // vm errors do not effect consensus and are therefore not assigned to err
+ result vm.GasBudget
+
+ // Capture the forwarded regular-gas amount BEFORE ForwardAll consumes
+ // it, so Absorb can back out state-gas spillover from UsedRegularGas
+ // per EIP-8037.
+ forwarded = st.gasRemaining.RegularGas
)
if contractCreation {
- ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value)
+ // Check whether the init code size has been exceeded.
+ if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil {
+ return nil, err
+ }
+ // Execute the transaction's creation.
+ ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
+ st.gasRemaining.Absorb(result, forwarded)
+
+ // If the contract creation failed, refund the account-creation state
+ // gas pre-charged in IntrinsicGas.
+ if rules.IsAmsterdam && vmerr != nil {
+ st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
+ }
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
// Apply EIP-7702 authorizations.
- if msg.SetCodeAuthorizations != nil {
- for _, auth := range msg.SetCodeAuthorizations {
- // Note errors are ignored, we simply skip invalid authorizations here.
- st.applyAuthorization(&auth)
- }
- }
+ st.applyAuthorizations(rules, msg.SetCodeAuthorizations)
// Perform convenience warming of sender's delegation target. Although the
// sender is already warmed in Prepare(..), it's possible a delegation to
@@ -638,44 +709,18 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok {
st.state.AddAddressToAccessList(addr)
}
-
// Execute the transaction's call.
- ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
+ ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
+ st.gasRemaining.Absorb(result, forwarded)
}
- // Record the gas used excluding gas refunds. This value represents the actual
- // gas allowance required to complete execution.
- peakGasUsed := st.gasUsed()
-
- // Compute refund counter, capped to a refund quotient.
- st.gasRemaining.Refund(st.calcRefund())
-
- if rules.IsPrague {
- // After EIP-7623: Data-heavy transactions pay the floor gas.
- if used := st.gasUsed(); used < floorDataGas {
- prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used})
- if st.evm.Config.Tracer.HasGasHook() {
- st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor)
- }
- }
- if peakGasUsed < floorDataGas {
- peakGasUsed = floorDataGas
- }
- }
- // Return gas to the user
- st.returnGas()
-
- // Return gas to the gas pool
- if rules.IsAmsterdam {
- // Refund is excluded for returning
- err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed())
- } else {
- // Refund is included for returning
- err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed())
- }
+ // Settle down the gas usage and refund the ETH back if any remaining
+ gasUsed, peakUsed, err := st.settleGas(rules, floorDataGas)
if err != nil {
return nil, err
}
+
+ // Pay the effective transaction fee to the specific coinbase
effectiveTip := msg.GasPrice
if rules.IsLondon {
baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee)
@@ -684,13 +729,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee)
}
-
if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
// Skip fee payment when NoBaseFee is set and the fee fields
// are 0. This avoids a negative effectiveTip being applied to
// the coinbase when simulating calls.
} else {
- fee := new(uint256.Int).SetUint64(st.gasUsed())
+ fee := new(uint256.Int).SetUint64(gasUsed)
fee.Mul(fee, effectiveTip)
st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee)
@@ -699,19 +743,105 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64)
}
}
+
+ // 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: st.gasUsed(),
- MaxUsedGas: peakGasUsed,
+ UsedGas: gasUsed,
+ MaxUsedGas: peakUsed,
Err: vmerr,
ReturnData: ret,
}, nil
}
+// settleGas finalizes the per-tx gas accounting after EVM execution:
+//
+// - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas,
+// tx_state_gas) before any refund or floor:
+//
+// tx_gas_used_before_refund = tx.gas - gas_left - state_gas_reservoir
+// tx_state_gas = state_gas_used
+// tx_regular_gas = tx_gas_used_before_refund - tx_state_gas
+//
+// - Computes the receipt scalar tx_gas_used by applying the EIP-3529
+// refund (capped at tx_gas_used_before_refund/5) and the EIP-7623
+// calldata floor:
+//
+// tx_gas_used = max(tx_gas_used_before_refund - tx_gas_refund, calldata_floor)
+//
+// - Charges the block gas pool (2D under Amsterdam, scalar pre-Amsterdam).
+//
+// - Refunds the leftover gas to the sender as ETH.
+//
+// Returns the receipt-level tx_gas_used and the pre-refund peak (consumed
+// by gas-estimation callers via ExecutionResult.MaxUsedGas). UsedStateGas
+// should never become negative in the top-most frame, since state-gas
+// refunds occur only when state creation is reverted within the same
+// transaction and clearing pre-existing state is never refunded.
+func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (gasUsed, peakUsed uint64, err error) {
+ if st.gasRemaining.UsedStateGas < 0 {
+ return 0, 0, fmt.Errorf("negative topmost frame state gas usage, %d", st.gasRemaining.UsedStateGas)
+ }
+ txStateGas := uint64(st.gasRemaining.UsedStateGas)
+
+ // EIP-8037:
+ // tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir
+ // tx_state_gas = intrinsic_state_gas + tx_output.execution_state_gas_used
+ // tx_regular_gas = tx_gas_used_before_refund - tx_state_gas
+ gasLeft := st.gasRemaining.RegularGas + st.gasRemaining.StateGas
+ gasUsedBeforeRefund := st.msg.GasLimit - gasLeft
+
+ if gasUsedBeforeRefund < txStateGas {
+ return 0, 0, fmt.Errorf("negative topmost frame regular gas usage, total: %d, state: %d", gasUsedBeforeRefund, txStateGas)
+ }
+ txRegularGas := gasUsedBeforeRefund - txStateGas
+
+ // EIP-3529: tx_gas_refund = min(tx_gas_used_before_refund/5, refund_counter).
+ refund := st.calcRefund(gasUsedBeforeRefund)
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft + refund}, tracing.GasChangeTxRefunds)
+ }
+ gasLeft += refund
+ gasUsed = gasUsedBeforeRefund - refund
+
+ // EIP-7623: tx_gas_used = max(tx_gas_used_after_refund, calldata_floor).
+ peakUsed = gasUsedBeforeRefund
+ if rules.IsPrague && gasUsed < floorDataGas {
+ diff := floorDataGas - gasUsed
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{Regular: gasLeft - diff}, tracing.GasChangeTxDataFloor)
+ }
+ gasLeft -= diff
+ gasUsed = floorDataGas
+ peakUsed = max(peakUsed, floorDataGas)
+ }
+
+ if rules.IsAmsterdam {
+ if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil {
+ return 0, 0, err
+ }
+ } else {
+ if err = st.gp.ChargeGasLegacy(gasLeft, gasUsed); err != nil {
+ return 0, 0, err
+ }
+ }
+
+ // Refund leftover gas to the sender as ETH.
+ if gasLeft > 0 {
+ refund := new(uint256.Int).Mul(uint256.NewInt(gasLeft), st.msg.GasPrice)
+ st.state.AddBalance(st.msg.From, refund, tracing.BalanceIncreaseGasReturn)
+
+ if st.evm.Config.Tracer.HasGasHook() {
+ st.evm.Config.Tracer.EmitGasChange(tracing.Gas{Regular: gasLeft}, tracing.Gas{}, tracing.GasChangeTxLeftOverReturned)
+ }
+ }
+ return gasUsed, peakUsed, nil
+}
+
// validateAuthorization validates an EIP-7702 authorization against the state.
func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
// Verify chain ID is null or equal to current chain ID.
@@ -743,72 +873,93 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio
return authority, nil
}
-// applyAuthorization applies an EIP-7702 code delegation to the state.
-func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error {
+// 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.
+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)
+ }
return err
}
+ prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority))
- // If the account already exists in state, refund the new account cost
- // charged in the intrinsic calculation.
- if st.state.Exist(authority) {
- st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
+ if !rules.IsAmsterdam {
+ if st.state.Exist(authority) {
+ st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
+ }
+ } else {
+ if st.state.Exist(authority) {
+ st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
+ }
+ authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte
+
+ preDelegated, ok := delegates[authority]
+ if !ok {
+ preDelegated = curDelegated
+ delegates[authority] = preDelegated
+ }
+ if auth.Address == (common.Address{}) {
+ // Clearing writes no indicator, refill this auth's state charge.
+ st.gasRemaining.RefundState(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)
+ }
+ } 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)
+ }
}
// Update nonce and account code.
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
+
+ // Delegation to zero address means clear.
if auth.Address == (common.Address{}) {
- // Delegation to zero address means clear.
- st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
+ if curDelegated {
+ st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
+ }
return nil
}
-
- // Otherwise install delegation to auth.Address.
- st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
-
+ // Install delegation to auth.Address if the delegation changed
+ if !curDelegated || auth.Address != prevDelegation {
+ st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
+ }
return nil
}
-// calcRefund computes refund counter, capped to a refund quotient.
-func (st *stateTransition) calcRefund() vm.GasBudget {
- var refund uint64
- if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
- // Before EIP-3529: refunds were capped to gasUsed / 2
- refund = st.gasUsed() / params.RefundQuotient
- } else {
- // After EIP-3529: refunds are capped to gasUsed / 5
- refund = st.gasUsed() / params.RefundQuotientEIP3529
+// applyAuthorizations applies an EIP-7702 code delegation to the state.
+func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) {
+ preDelegated := make(map[common.Address]bool)
+ for _, auth := range auths {
+ st.applyAuthorization(rules, &auth, preDelegated)
}
+}
+
+// calcRefund computes the EIP-3529 refund cap against tx_gas_used_before_refund.
+func (st *stateTransition) calcRefund(gasUsedBeforeRefund uint64) uint64 {
+ quotient := params.RefundQuotient
+ if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
+ quotient = params.RefundQuotientEIP3529
+ }
+ refund := gasUsedBeforeRefund / quotient
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
- if refund > 0 && st.evm.Config.Tracer.HasGasHook() {
- after := st.gasRemaining
- after.RegularGas += refund
-
- st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds)
- }
- return vm.NewGasBudget(refund)
-}
-
-// returnGas returns ETH for remaining gas,
-// exchanged at the original rate.
-func (st *stateTransition) returnGas() {
- remaining := uint256.NewInt(st.gasRemaining.RegularGas)
- remaining.Mul(remaining, st.msg.GasPrice)
- st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
-
- if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() {
- after := st.gasRemaining
- after.RegularGas = 0
- st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned)
- }
-}
-
-// gasUsed returns the amount of gas used up by the state transition.
-func (st *stateTransition) gasUsed() uint64 {
- return st.gasRemaining.Used(st.initialBudget)
+ return refund
}
// blobGasUsed returns the amount of blob gas used by the message.
diff --git a/core/state_transition_test.go b/core/state_transition_test.go
index 8aab016123..be2de7f511 100644
--- a/core/state_transition_test.go
+++ b/core/state_transition_test.go
@@ -155,50 +155,50 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028 bool
isEIP3860 bool
isAmsterdam bool
- want uint64
+ want vm.GasCosts
}{
{
name: "frontier/empty-call",
- want: params.TxGas,
+ want: vm.GasCosts{RegularGas: params.TxGas},
},
{
name: "frontier/contract-creation-pre-homestead",
creation: true,
isHomestead: false,
// pre-homestead, contract creation still uses TxGas
- want: params.TxGas,
+ want: vm.GasCosts{RegularGas: params.TxGas},
},
{
name: "homestead/contract-creation",
creation: true,
isHomestead: true,
- want: params.TxGasContractCreation,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation},
},
{
name: "frontier/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100),
// 100 nz bytes * 68 (frontier)
- want: params.TxGas + 100*params.TxDataNonZeroGasFrontier,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasFrontier},
},
{
name: "istanbul/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100),
isEIP2028: true,
// 100 nz bytes * 16 (post-EIP2028)
- want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasEIP2028},
},
{
name: "istanbul/zero-data",
data: bytes.Repeat([]byte{0x00}, 100),
isEIP2028: true,
// 100 zero bytes * 4
- want: params.TxGas + 100*params.TxDataZeroGas,
+ want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas},
},
{
name: "istanbul/mixed-data",
data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
isEIP2028: true,
- want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028,
+ want: vm.GasCosts{RegularGas: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028},
},
{
name: "shanghai/init-code-word-gas",
@@ -208,7 +208,7 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true,
isEIP3860: true,
// TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2
- want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas},
},
{
name: "shanghai/init-code-non-multiple-of-32",
@@ -217,7 +217,7 @@ func TestIntrinsicGas(t *testing.T) {
isHomestead: true,
isEIP2028: true,
isEIP3860: true,
- want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas},
},
{
name: "berlin/access-list",
@@ -227,7 +227,7 @@ func TestIntrinsicGas(t *testing.T) {
},
isEIP2028: true,
// 2 addrs * 2400 + 3 keys * 1900
- want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas,
+ want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas},
},
{
name: "amsterdam/access-list-extra-cost",
@@ -238,9 +238,9 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true,
isAmsterdam: true,
// base access-list charge + EIP-7981 extra
- want: params.TxGas +
+ want: vm.GasCosts{RegularGas: params.TxGas +
2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas +
- 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost,
+ 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost},
},
{
name: "prague/auth-list",
@@ -250,8 +250,54 @@ func TestIntrinsicGas(t *testing.T) {
{Address: addr1},
},
isEIP2028: true,
- // 3 auths * 25000
- want: params.TxGas + 3*params.CallNewAccountGas,
+ // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple)
+ want: vm.GasCosts{RegularGas: params.TxGas + 3*params.CallNewAccountGas},
+ },
+ {
+ name: "amsterdam/contract-creation-empty",
+ creation: true,
+ 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.
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
+ },
+ {
+ name: "amsterdam/contract-creation-init-code",
+ data: bytes.Repeat([]byte{0x00}, 64), // 2 words of init code
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true, // Shanghai gates init-code word gas
+ isAmsterdam: true,
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam +
+ 64*params.TxDataZeroGas + 2*params.InitCodeWordGas,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
+ },
+ {
+ name: "amsterdam/contract-creation-with-access-list",
+ data: bytes.Repeat([]byte{0xff}, 32), // 1 word of non-zero init code
+ accessList: types.AccessList{
+ {Address: addr1, StorageKeys: []common.Hash{key1}},
+ },
+ creation: true,
+ isHomestead: true,
+ isEIP2028: true,
+ isEIP3860: true,
+ isAmsterdam: true,
+ want: vm.GasCosts{
+ RegularGas: params.TxGas + params.CreateGasAmsterdam +
+ 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas +
+ 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost,
+ StateGas: params.AccountCreationSize * params.CostPerStateByte,
+ },
},
{
name: "amsterdam/combined",
@@ -264,23 +310,34 @@ func TestIntrinsicGas(t *testing.T) {
},
isEIP2028: true,
isAmsterdam: true,
- want: params.TxGas +
- 100*params.TxDataNonZeroGasEIP2028 +
- 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
- 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
- 1*params.CallNewAccountGas,
+ // EIP-8037 splits the auth-tuple charge into regular + state gas:
+ // regular: TxAuthTupleRegularGas (7500) per auth
+ // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth
+ want: vm.GasCosts{
+ RegularGas: params.TxGas +
+ 100*params.TxDataNonZeroGasEIP2028 +
+ 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
+ 1*params.TxAuthTupleRegularGas,
+ StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte,
+ },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ rules := params.Rules{
+ IsHomestead: tt.isHomestead,
+ IsIstanbul: tt.isEIP2028,
+ IsShanghai: tt.isEIP3860,
+ IsAmsterdam: tt.isAmsterdam,
+ }
got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList,
- tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam)
+ tt.creation, rules, params.CostPerStateByte)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
- want := vm.GasCosts{RegularGas: tt.want}
- if got != want {
- t.Fatalf("gas mismatch: got %+v, want %+v", got, want)
+ if got != tt.want {
+ t.Fatalf("gas mismatch: got %+v, want %+v", got, tt.want)
}
})
}
diff --git a/core/tracing/gen_gas_change_reason_stringer.go b/core/tracing/gen_gas_change_reason_stringer.go
index e64be781a6..3d3aa96fad 100644
--- a/core/tracing/gen_gas_change_reason_stringer.go
+++ b/core/tracing/gen_gas_change_reason_stringer.go
@@ -28,21 +28,22 @@ func _() {
_ = x[GasChangeWitnessCodeChunk-17]
_ = x[GasChangeWitnessContractCollisionCheck-18]
_ = x[GasChangeTxDataFloor-19]
+ _ = x[GasChangeAccountCreation-20]
_ = x[GasChangeIgnored-255]
}
const (
- _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloor"
+ _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorAccountCreation"
_GasChangeReason_name_1 = "Ignored"
)
var (
- _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353}
+ _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 368}
)
func (i GasChangeReason) String() string {
switch {
- case i <= 19:
+ case i <= 20:
return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]]
case i == 255:
return _GasChangeReason_name_1
diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go
index 6ea3f7ebbf..78b85fafea 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -472,6 +472,10 @@ const (
// transaction data. This change will always be a negative change.
GasChangeTxDataFloor GasChangeReason = 19
+ // GasChangeAccountCreation represents the state gas charging for account
+ // creation inside the call/create frame.
+ GasChangeAccountCreation GasChangeReason = 20
+
// GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as
// it will be "manually" tracked by a direct emit of the gas change event.
GasChangeIgnored GasChangeReason = 0xFF
diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go
index 090d250e65..b918925bd2 100644
--- a/core/txpool/blobpool/blobpool.go
+++ b/core/txpool/blobpool/blobpool.go
@@ -279,10 +279,9 @@ func encodeForNetwork(storedRLP []byte, version uint) ([]byte, error) {
// 3. Find the version of sidecar.
sidecarVersion, _, err := rlp.SplitUint64(sidecarElems[0])
- if err != nil || sidecarVersion > 255 {
+ if err != nil || sidecarVersion > 255 || sidecarVersion == 0 {
return nil, fmt.Errorf("invalid version: %w", err)
}
- sidecarVersionByte := byte(sidecarVersion)
// 4. Extract sidecar elements.
commitmentsRLP := sidecarElems[2]
@@ -315,12 +314,7 @@ func encodeForNetwork(storedRLP []byte, version uint) ([]byte, error) {
}
// 6. Reconstruct into the network format.
- var outer [][]byte
- if sidecarVersionByte == types.BlobSidecarVersion0 {
- outer = [][]byte{txRLP, blobsField, commitmentsRLP, proofsRLP}
- } else {
- outer = [][]byte{txRLP, sidecarElems[0], blobsField, commitmentsRLP, proofsRLP}
- }
+ outer := [][]byte{txRLP, sidecarElems[0], blobsField, commitmentsRLP, proofsRLP}
body, err := rlp.MergeListValues(outer)
if err != nil {
return nil, err
@@ -634,7 +628,7 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
p.state = state
// Create new slotter for pre-Osaka blob configuration.
- slotter := newSlotter(params.BlobTxMaxBlobs)
+ slotter := newSlotterEIP7594(params.BlobTxMaxBlobs)
// See if we need to migrate the queue blob store after fusaka
slotter, err = tryMigrate(p.chain.Config(), slotter, queuedir)
diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go
index 4e1672f22a..394b9e31d4 100644
--- a/core/txpool/blobpool/blobpool_test.go
+++ b/core/txpool/blobpool/blobpool_test.go
@@ -53,7 +53,6 @@ import (
var (
testBlobs []*kzg4844.Blob
testBlobCommits []kzg4844.Commitment
- testBlobProofs []kzg4844.Proof
testBlobCellProofs [][]kzg4844.Proof
testBlobVHashes [][32]byte
testBlobIndices = make(map[[32]byte]int)
@@ -69,9 +68,6 @@ func init() {
testBlobCommit, _ := kzg4844.BlobToCommitment(testBlob)
testBlobCommits = append(testBlobCommits, testBlobCommit)
- testBlobProof, _ := kzg4844.ComputeBlobProof(testBlob, testBlobCommit)
- testBlobProofs = append(testBlobProofs, testBlobProof)
-
testBlobCellProof, _ := kzg4844.ComputeCellProofs(testBlob)
testBlobCellProofs = append(testBlobCellProofs, testBlobCellProof)
@@ -244,7 +240,7 @@ func encodeForPool(tx *types.Transaction) []byte {
// makeMultiBlobTx is a utility method to construct a random blob tx with
// certain number of blobs in its sidecar.
-func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction {
+func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey) *types.Transaction {
var (
blobs []kzg4844.Blob
blobHashes []common.Hash
@@ -254,12 +250,8 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
for i := 0; i < blobCount; i++ {
blobs = append(blobs, *testBlobs[blobOffset+i])
commitments = append(commitments, testBlobCommits[blobOffset+i])
- if version == types.BlobSidecarVersion0 {
- proofs = append(proofs, testBlobProofs[blobOffset+i])
- } else {
- cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i])
- proofs = append(proofs, cellProofs...)
- }
+ cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i])
+ proofs = append(proofs, cellProofs...)
blobHashes = append(blobHashes, testBlobVHashes[blobOffset+i])
}
blobtx := &types.BlobTx{
@@ -271,7 +263,7 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
BlobFeeCap: uint256.NewInt(blobFeeCap),
BlobHashes: blobHashes,
Value: uint256.NewInt(100),
- Sidecar: types.NewBlobTxSidecar(version, blobs, commitments, proofs),
+ Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion1, blobs, commitments, proofs),
}
return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx)
}
@@ -303,7 +295,7 @@ func makeUnsignedTxWithTestBlob(nonce uint64, gasTipCap uint64, gasFeeCap uint64
BlobFeeCap: uint256.NewInt(blobFeeCap),
BlobHashes: []common.Hash{testBlobVHashes[blobIdx]},
Value: uint256.NewInt(100),
- Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion0, []kzg4844.Blob{*testBlobs[blobIdx]}, []kzg4844.Commitment{testBlobCommits[blobIdx]}, []kzg4844.Proof{testBlobProofs[blobIdx]}),
+ Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion1, []kzg4844.Blob{*testBlobs[blobIdx]}, []kzg4844.Commitment{testBlobCommits[blobIdx]}, testBlobCellProofs[blobIdx]),
}
}
@@ -450,36 +442,18 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
hashes = append(hashes, tx.vhashes...)
}
}
- blobs1, _, proofs1, err := pool.getBlobs(hashes, types.BlobSidecarVersion0)
+ blobs, _, proofs, err := pool.getBlobs(hashes, types.BlobSidecarVersion1)
if err != nil {
t.Fatal(err)
}
- blobs2, _, proofs2, err := pool.getBlobs(hashes, types.BlobSidecarVersion1)
- if err != nil {
- t.Fatal(err)
- }
- // Cross validate what we received vs what we wanted
- if len(blobs1) != len(hashes) || len(proofs1) != len(hashes) {
- t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs1), len(proofs1), len(hashes))
- return
- }
- if len(blobs2) != len(hashes) || len(proofs2) != len(hashes) {
- t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want blobs %d, want proofs: %d", len(blobs2), len(proofs2), len(hashes), len(hashes))
+ if len(blobs) != len(hashes) || len(proofs) != len(hashes) {
+ t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want blobs %d, want proofs: %d", len(blobs), len(proofs), len(hashes), len(hashes))
return
}
for i, hash := range hashes {
- // If an item is missing from both, but shouldn't, error
- if (blobs1[i] == nil || proofs1[i] == nil) && (blobs2[i] == nil || proofs2[i] == nil) {
- t.Errorf("tracked blob retrieval failed: item %d, hash %x", i, hash)
- continue
- }
// Item retrieved, make sure it matches the expectation
index := testBlobIndices[hash]
- if blobs1[i] != nil && (*blobs1[i] != *testBlobs[index] || proofs1[i][0] != testBlobProofs[index]) {
- t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
- continue
- }
- if blobs2[i] != nil && (*blobs2[i] != *testBlobs[index] || !slices.Equal(proofs2[i], testBlobCellProofs[index])) {
+ if blobs[i] != nil && (*blobs[i] != *testBlobs[index] || !slices.Equal(proofs[i], testBlobCellProofs[index])) {
t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
continue
}
@@ -513,7 +487,7 @@ func TestOpenDrops(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(testMaxBlobsPerBlock), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
// Insert a malformed transaction to verify that decoding errors (or format
// changes) are handled gracefully (case 1)
@@ -841,7 +815,7 @@ func TestOpenIndex(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(testMaxBlobsPerBlock), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
// Insert a sequence of transactions with varying price points to check that
// the cumulative minimum will be maintained.
@@ -929,7 +903,7 @@ func TestOpenHeap(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(testMaxBlobsPerBlock), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
// Insert a few transactions from a few accounts. To remove randomness from
// the heap initialization, use a deterministic account/tx/priority ordering.
@@ -1107,7 +1081,7 @@ func TestChangingSlotterSize(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(6), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(6), nil)
// Create transactions from a few accounts.
var (
@@ -1119,9 +1093,9 @@ func TestChangingSlotterSize(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
- tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0)
- tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0)
- tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0)
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
+ tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2)
+ tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3)
blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2)
@@ -1222,9 +1196,9 @@ func TestBillyMigration(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
- tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0)
- tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0)
- tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0)
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
+ tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2)
+ tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3)
blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2)
@@ -1318,7 +1292,7 @@ func TestLegacyTxConversion(t *testing.T) {
// Initialize the pending store with two blob transactions encoded in the
// legacy format.
queuedir := filepath.Join(storage, pendingTransactionStore)
- store, err := billy.Open(billy.Options{Path: queuedir}, newSlotter(testMaxBlobsPerBlock), nil)
+ store, err := billy.Open(billy.Options{Path: queuedir}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
if err != nil {
t.Fatalf("failed to open billy: %v", err)
}
@@ -1328,8 +1302,8 @@ func TestLegacyTxConversion(t *testing.T) {
addr1 := crypto.PubkeyToAddress(key1.PublicKey)
addr2 := crypto.PubkeyToAddress(key2.PublicKey)
- tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1, types.BlobSidecarVersion0)
- tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2, types.BlobSidecarVersion0)
+ tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1)
+ tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2)
for _, tx := range []*types.Transaction{tx1, tx2} {
legacy, err := rlp.EncodeToBytes(tx)
@@ -1427,8 +1401,8 @@ func TestBlobCountLimit(t *testing.T) {
// Attempt to add transactions.
var (
- tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0)
- tx2 = makeMultiBlobTx(0, 1, 800, 70, 7, 0, key2, types.BlobSidecarVersion0)
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
+ tx2 = makeMultiBlobTx(0, 1, 800, 70, 7, 0, key2)
)
errs := pool.Add([]*types.Transaction{tx1, tx2}, true)
@@ -1830,7 +1804,7 @@ func TestAdd(t *testing.T) {
storage := filepath.Join(t.TempDir(), fmt.Sprintf("test-%d", i))
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(testMaxBlobsPerBlock), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
// Insert the seed transactions for the pool startup
var (
@@ -1941,7 +1915,7 @@ func TestGetBlobs(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(params.BlobTxMaxBlobs), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
// Create transactions from a few accounts.
var (
@@ -1953,9 +1927,9 @@ func TestGetBlobs(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
- tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0) // [0, 6)
- tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion1) // [6, 12)
- tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3, types.BlobSidecarVersion0) // [12, 18)
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1) // [0, 6)
+ tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2) // [6, 12)
+ tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3) // [12, 18)
blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2)
@@ -2023,7 +1997,7 @@ func TestGetBlobs(t *testing.T) {
}{
{
start: 0, limit: 6,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 0, limit: 6,
@@ -2031,7 +2005,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 0, limit: 6, fillRandom: true,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 0, limit: 6, fillRandom: true,
@@ -2039,7 +2013,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 3, limit: 9,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 3, limit: 9,
@@ -2047,7 +2021,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 3, limit: 9, fillRandom: true,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 3, limit: 9, fillRandom: true,
@@ -2055,7 +2029,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 3, limit: 15,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 3, limit: 15,
@@ -2063,7 +2037,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 3, limit: 15, fillRandom: true,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 3, limit: 15, fillRandom: true,
@@ -2071,7 +2045,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 0, limit: 18,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 0, limit: 18,
@@ -2079,7 +2053,7 @@ func TestGetBlobs(t *testing.T) {
},
{
start: 0, limit: 18, fillRandom: true,
- version: types.BlobSidecarVersion0,
+ version: types.BlobSidecarVersion1,
},
{
start: 0, limit: 18, fillRandom: true,
@@ -2133,7 +2107,7 @@ func TestGetBlobs(t *testing.T) {
if blobs[j] == nil || proofs[j] == nil {
// This is only an error if there was no version mismatch
if (c.version == types.BlobSidecarVersion1 && 6 <= testBlobIndex && testBlobIndex < 12) ||
- (c.version == types.BlobSidecarVersion0 && (testBlobIndex < 6 || 12 <= testBlobIndex)) {
+ (c.version == types.BlobSidecarVersion1 && (testBlobIndex < 6 || 12 <= testBlobIndex)) {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
}
continue
@@ -2144,15 +2118,9 @@ func TestGetBlobs(t *testing.T) {
continue
}
// Item retrieved, make sure the proof matches the expectation
- if c.version == types.BlobSidecarVersion0 {
- if proofs[j][0] != testBlobProofs[testBlobIndex] {
- t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
- }
- } else {
- want, _ := kzg4844.ComputeCellProofs(blobs[j])
- if !reflect.DeepEqual(want, proofs[j]) {
- t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
- }
+ want, _ := kzg4844.ComputeCellProofs(blobs[j])
+ if !reflect.DeepEqual(want, proofs[j]) {
+ t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
}
}
}
@@ -2165,25 +2133,22 @@ func TestGetBlobs(t *testing.T) {
// - eth/72: blob payload omitted, output matches removeBlobs(tx)
func TestEncodeForNetwork(t *testing.T) {
cases := []struct {
- name string
- sidecarVer byte
- ethVer uint
+ name string
+ ethVer uint
}{
- {"v0/eth70", types.BlobSidecarVersion0, 70},
- {"v1/eth70", types.BlobSidecarVersion1, 70},
- {"v0/eth72", types.BlobSidecarVersion0, 72},
- {"v1/eth72", types.BlobSidecarVersion1, 72},
+ {"eth70", 70},
+ {"eth72", 72},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
- testEncodeForNetwork(t, tc.sidecarVer, tc.ethVer)
+ testEncodeForNetwork(t, tc.ethVer)
})
}
}
-func testEncodeForNetwork(t *testing.T, sidecarVer byte, ethVer uint) {
+func testEncodeForNetwork(t *testing.T, ethVer uint) {
key, _ := crypto.GenerateKey()
- tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, sidecarVer)
+ tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key)
wantTx := tx
if ethVer >= 72 {
@@ -2293,14 +2258,14 @@ func TestGetCells(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
- store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(params.BlobTxMaxBlobs), nil)
+ store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
var (
key1, _ = crypto.GenerateKey()
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
- tx1 = makeMultiBlobTx(0, 1, 1000, 100, 3, 0, key1, types.BlobSidecarVersion1) // blobs [0, 3)
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 3, 0, key1) // blobs [0, 3)
pooledTx1, _ = newBlobTxForPool(tx1)
diff --git a/core/txpool/blobpool/buffer_test.go b/core/txpool/blobpool/buffer_test.go
index ba2e74bd2e..beba17f0a4 100644
--- a/core/txpool/blobpool/buffer_test.go
+++ b/core/txpool/blobpool/buffer_test.go
@@ -13,7 +13,7 @@ import (
// (simulating what ETH/72 peers send).
func makeV1Tx(t *testing.T, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey) *types.Transaction {
t.Helper()
- tx := makeMultiBlobTx(nonce, 1, 1, 1, blobCount, blobOffset, key, types.BlobSidecarVersion1)
+ tx := makeMultiBlobTx(nonce, 1, 1, 1, blobCount, blobOffset, key)
return removeBlobs(tx)
}
diff --git a/core/txpool/blobpool/cache_test.go b/core/txpool/blobpool/cache_test.go
index f7a86317cd..a410208bcc 100644
--- a/core/txpool/blobpool/cache_test.go
+++ b/core/txpool/blobpool/cache_test.go
@@ -58,7 +58,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache {
if err := os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700); err != nil {
t.Fatalf("mkdir: %v", err)
}
- store, err := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(params.BlobTxMaxBlobs), nil)
+ store, err := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
if err != nil {
t.Fatalf("billy open: %v", err)
}
@@ -70,7 +70,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache {
)
for _, s := range txConfig {
key, _ := crypto.GenerateKey()
- tx := makeMultiBlobTx(0, s.tip, 1_000_000, 1_000_000, s.blobs, offset, key, types.BlobSidecarVersion1)
+ tx := makeMultiBlobTx(0, s.tip, 1_000_000, 1_000_000, s.blobs, offset, key)
if _, err := store.Put(encodeForPool(tx)); err != nil {
t.Fatalf("store put: %v", err)
}
@@ -143,7 +143,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache {
func (tc *testCache) inject(t *testing.T, spec txSpec) []common.Hash {
t.Helper()
key, _ := crypto.GenerateKey()
- tx := makeMultiBlobTx(0, spec.tip, 1_000_000, 1_000_000, spec.blobs, tc.offset, key, types.BlobSidecarVersion1)
+ tx := makeMultiBlobTx(0, spec.tip, 1_000_000, 1_000_000, spec.blobs, tc.offset, key)
tc.offset += spec.blobs
ptx, err := newBlobTxForPool(tx)
diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go
index 07520d534f..dc091132bb 100644
--- a/core/txpool/blobpool/limbo.go
+++ b/core/txpool/blobpool/limbo.go
@@ -56,7 +56,7 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
}
// Create new slotter for pre-Osaka blob configuration.
- slotter := newSlotter(params.BlobTxMaxBlobs)
+ slotter := newSlotterEIP7594(params.BlobTxMaxBlobs)
// See if we need to migrate the limbo after fusaka.
slotter, err := tryMigrate(config, slotter, datadir)
diff --git a/core/txpool/blobpool/slotter.go b/core/txpool/blobpool/slotter.go
index 3399361e55..49a46d4f42 100644
--- a/core/txpool/blobpool/slotter.go
+++ b/core/txpool/blobpool/slotter.go
@@ -58,27 +58,6 @@ func tryMigrate(config *params.ChainConfig, slotter billy.SlotSizeFn, datadir st
return slotter, nil
}
-// newSlotter creates a helper method for the Billy datastore that returns the
-// individual shelf sizes used to store transactions in.
-//
-// The slotter will create shelves for each possible blob count + some tx metadata
-// wiggle room, up to the max permitted limits.
-//
-// The slotter also creates a shelf for 0-blob transactions. Whilst those are not
-// allowed in the current protocol, having an empty shelf is not a relevant use
-// of resources, but it makes stress testing with junk transactions simpler.
-func newSlotter(maxBlobsPerTransaction int) billy.SlotSizeFn {
- slotsize := uint32(txAvgSize)
- slotsize -= uint32(blobSize) // underflows, it's ok, will overflow back in the first return
-
- return func() (size uint32, done bool) {
- slotsize += blobSize
- finished := slotsize > uint32(maxBlobsPerTransaction)*blobSize+txMaxSize
-
- return slotsize, finished
- }
-}
-
// newSlotterEIP7594 creates a different slotter for EIP-7594 transactions.
// EIP-7594 (PeerDAS) changes the average transaction size which means the current
// static 4KB average size is not enough anymore.
diff --git a/core/txpool/blobpool/slotter_test.go b/core/txpool/blobpool/slotter_test.go
index e4cf232f4e..24ad2b5330 100644
--- a/core/txpool/blobpool/slotter_test.go
+++ b/core/txpool/blobpool/slotter_test.go
@@ -20,47 +20,6 @@ import (
"testing"
)
-// Tests that the slotter creates the expected database shelves.
-func TestNewSlotter(t *testing.T) {
- // Generate the database shelve sizes
- slotter := newSlotter(6)
-
- var shelves []uint32
- for {
- shelf, done := slotter()
- shelves = append(shelves, shelf)
- if done {
- break
- }
- }
- // Compare the database shelves to the expected ones
- want := []uint32{
- 0*blobSize + txAvgSize, // 0 blob + some expected tx infos
- 1*blobSize + txAvgSize, // 1 blob + some expected tx infos
- 2*blobSize + txAvgSize, // 2 blob + some expected tx infos (could be fewer blobs and more tx data)
- 3*blobSize + txAvgSize, // 3 blob + some expected tx infos (could be fewer blobs and more tx data)
- 4*blobSize + txAvgSize, // 4 blob + some expected tx infos (could be fewer blobs and more tx data)
- 5*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 6*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 7*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 8*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 9*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 10*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 11*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 12*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 13*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
- 14*blobSize + txAvgSize, // 1-6 blobs + unexpectedly large tx infos >= 4 blobs + max tx metadata size
- }
- if len(shelves) != len(want) {
- t.Errorf("shelves count mismatch: have %d, want %d", len(shelves), len(want))
- }
- for i := 0; i < len(shelves) && i < len(want); i++ {
- if shelves[i] != want[i] {
- t.Errorf("shelf %d mismatch: have %d, want %d", i, shelves[i], want[i])
- }
- }
-}
-
// Tests that the slotter creates the expected database shelves.
func TestNewSlotterEIP7594(t *testing.T) {
// Generate the database shelve sizes
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index c4cc213982..fa148797ef 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -122,7 +122,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata
- intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
return err
}
@@ -135,22 +135,29 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if err != nil {
return err
}
+ // Make sure the transaction has sufficient gas allowance to
+ // pay the floor cost.
if tx.Gas() < floorDataGas {
return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas)
}
+ // In Amsterdam, the transaction gas limit is allowed to exceed
+ // params.MaxTxGas, but the calldata floor cost is capped by it.
+ if rules.IsAmsterdam && max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas {
+ return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas)
+ }
}
// Ensure the gasprice is high enough to cover the requirement of the calling pool
if tx.GasTipCapIntCmp(opts.MinTip) < 0 {
return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip)
}
+ if tx.Type() == types.BlobTxType {
+ return validateBlobSidecar(tx, head, opts)
+ }
if tx.Type() == types.SetCodeTxType {
if len(tx.SetCodeAuthorizations()) == 0 {
return errors.New("set code tx must have at least one authorization tuple")
}
}
- if tx.Type() == types.BlobTxType {
- return validateBlobSidecar(tx, head, opts)
- }
return nil
}
@@ -172,25 +179,11 @@ func validateBlobSidecar(tx *types.Transaction, head *types.Header, opts *Valida
if len(hashes) > opts.MaxBlobCount {
return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, len(hashes), opts.MaxBlobCount)
}
-
- expected := types.BlobSidecarVersion0
- if opts.Config.IsOsaka(head.Number, head.Time) {
- expected = types.BlobSidecarVersion1
+ if sidecar.Version != types.BlobSidecarVersion1 {
+ return fmt.Errorf("%w: unexpected sidecar version, want: %d, got: %d", ErrSidecarFormatError, types.BlobSidecarVersion1, sidecar.Version)
}
- if sidecar.Version != expected {
- return fmt.Errorf("%w: unexpected sidecar version, want: %d, got: %d", ErrSidecarFormatError, expected, sidecar.Version)
- }
-
- //todo: can we drop the support for v0 blob txs in blobpool?
- switch sidecar.Version {
- case types.BlobSidecarVersion0:
- if len(sidecar.Proofs) != len(sidecar.Commitments) {
- return fmt.Errorf("%w: invalid number of %d blob proofs expected %d", ErrSidecarFormatError, len(sidecar.Proofs), len(sidecar.Commitments))
- }
- case types.BlobSidecarVersion1:
- if len(sidecar.Proofs) != len(sidecar.Commitments)*kzg4844.CellProofsPerBlob {
- return fmt.Errorf("%w: invalid number of %d blob proofs expected %d", ErrSidecarFormatError, len(sidecar.Proofs), len(sidecar.Commitments)*kzg4844.CellProofsPerBlob)
- }
+ if len(sidecar.Proofs) != len(sidecar.Commitments)*kzg4844.CellProofsPerBlob {
+ return fmt.Errorf("%w: invalid number of %d blob proofs expected %d", ErrSidecarFormatError, len(sidecar.Proofs), len(sidecar.Commitments)*kzg4844.CellProofsPerBlob)
}
return nil
}
@@ -210,24 +203,10 @@ func ValidateCells(sidecar *types.BlobTxCellSidecar) error {
if blobCount != len(sidecar.Commitments) {
return fmt.Errorf("invalid number of %d blobs compared to %d commitments", blobCount, len(sidecar.Commitments))
}
- // Fork-specific sidecar checks, including proof verification.
- if sidecar.Version == types.BlobSidecarVersion1 {
- return validateCellsOsaka(sidecar)
+ if sidecar.Version != types.BlobSidecarVersion1 {
+ return fmt.Errorf("unexpected sidecar version, want: %d, got: %d", types.BlobSidecarVersion1, sidecar.Version)
}
- return validateCellsLegacy(sidecar)
-}
-
-func validateCellsLegacy(sidecar *types.BlobTxCellSidecar) error {
- blobs, err := kzg4844.RecoverBlobs(sidecar.Cells, sidecar.Custody.Indices())
- if err != nil {
- return fmt.Errorf("%w: %v", ErrKZGVerificationError, err)
- }
- for i := range blobs {
- if err := kzg4844.VerifyBlobProof(&blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil {
- return fmt.Errorf("%w: invalid blob %d: %v", ErrKZGVerificationError, i, err)
- }
- }
- return nil
+ return validateCellsOsaka(sidecar)
}
func validateCellsOsaka(sidecar *types.BlobTxCellSidecar) error {
diff --git a/core/vm/common.go b/core/vm/common.go
index 2d631f8a55..5059b4af37 100644
--- a/core/vm/common.go
+++ b/core/vm/common.go
@@ -112,6 +112,5 @@ func toWordSize(size uint64) uint64 {
if size > math.MaxUint64-31 {
return math.MaxUint64/32 + 1
}
-
return (size + 31) / 32
}
diff --git a/core/vm/contract.go b/core/vm/contract.go
index 45c879c80f..9155e9f84a 100644
--- a/core/vm/contract.go
+++ b/core/vm/contract.go
@@ -42,6 +42,8 @@ type Contract struct {
IsDeployment bool
IsSystemCall bool
+ // Gas carries the unified gas state for this frame: running balance,
+ // reservoir, and per-frame usage accumulators. See GasBudget.
Gas GasBudget
value *uint256.Int
}
@@ -113,7 +115,6 @@ func (c *Contract) GetOp(n uint64) OpCode {
if n < uint64(len(c.Code)) {
return OpCode(c.Code[n])
}
-
return STOP
}
@@ -125,9 +126,10 @@ func (c *Contract) Caller() common.Address {
return c.caller
}
-// UseGas attempts the use gas and subtracts it and returns true on success
-func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) {
- prior, ok := c.Gas.Charge(cost)
+// chargeRegular deducts regular gas only, with tracer integration.
+// Returns false on OOG. Delegates the arithmetic to GasBudget.ChargeRegular.
+func (c *Contract) chargeRegular(r uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
+ prior, ok := c.Gas.ChargeRegular(r)
if !ok {
return false
}
@@ -137,15 +139,44 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G
return true
}
-// RefundGas refunds the leftover gas budget back to the contract.
-func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
- prior, changed := c.Gas.Refund(refund)
- if !changed {
- return
+// chargeState deducts state gas (spilling into regular when the reservoir is
+// exhausted), with tracer integration. Returns false on OOG.
+func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
+ prior, ok := c.Gas.ChargeState(s)
+ if !ok {
+ return false
}
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
+ return true
+}
+
+// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
+func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
+ prior := c.Gas
+ c.Gas.Absorb(child, forwarded)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
+ }
+}
+
+// forwardGas drains `regular` regular gas and the entire state reservoir
+// from this contract's running budget and returns the initial GasBudget for
+// a child frame. The caller's UsedRegularGas is bumped by the forwarded
+// amount so that the absorb-on-return path correctly reclaims the unused
+// portion. Thin wrapper around GasBudget.Forward with tracer integration.
+//
+// Caller must ensure `regular` is no larger than the running balance (the
+// opcode's dynamic gas table is expected to validate that before invoking
+// the opcode handler).
+func (c *Contract) forwardGas(regular uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) GasBudget {
+ prior := c.Gas
+ child := c.Gas.Forward(regular)
+ if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
+ logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
+ }
+ return child
}
// Address returns the contracts address
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index 71cfdbc527..6908ffeba1 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -264,9 +264,8 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
// - any error that occurred
func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) {
gasCost := p.RequiredGas(input)
- prior, ok := gas.Charge(GasCosts{RegularGas: gasCost})
+ prior, ok := gas.ChargeRegular(gasCost)
if !ok {
- gas.Exhaust()
return nil, gas, ErrOutOfGas
}
if logger.HasGasHook() {
diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go
index 988cdb91f2..4d28df6a6a 100644
--- a/core/vm/contracts_fuzz_test.go
+++ b/core/vm/contracts_fuzz_test.go
@@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) {
return
}
inWant := string(input)
- RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{})
+ RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{})
if inHave := string(input); inWant != inHave {
t.Errorf("Precompiled %v modified input data", a)
}
diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go
index e7841c8552..c6975bd0a6 100644
--- a/core/vm/contracts_test.go
+++ b/core/vm/contracts_test.go
@@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil {
+ if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil {
t.Error(err)
} else if common.Bytes2Hex(res) != test.Expected {
t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res))
@@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) {
gas := test.Gas - 1
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{})
if err.Error() != "out of gas" {
t.Errorf("Expected error [out of gas], got [%v]", err)
}
@@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing
in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in)
t.Run(test.Name, func(t *testing.T) {
- _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{})
+ _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{})
if err.Error() != test.ExpectedError {
t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err)
}
@@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) {
start := time.Now()
for bench.Loop() {
copy(data, in)
- res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{})
+ res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{})
}
elapsed := uint64(time.Since(start))
if elapsed < 1 {
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 33af8fd4fd..f8473e65e8 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -43,6 +43,7 @@ var activators = map[int]func(*JumpTable){
7939: enable7939,
8024: enable8024,
7843: enable7843,
+ 8037: enable8037,
}
// EnableEIP enables the given EIP on the config.
@@ -211,8 +212,7 @@ func opTstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.readOnly {
return nil, ErrWriteProtection
}
- loc := scope.Stack.pop()
- val := scope.Stack.pop()
+ loc, val := scope.Stack.pop2()
evm.StateDB.SetTransientState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32())
return nil, nil
}
@@ -262,11 +262,7 @@ func enable5656(jt *JumpTable) {
// opMcopy implements the MCOPY opcode (https://eips.ethereum.org/EIPS/eip-5656)
func opMcopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- dst = scope.Stack.pop()
- src = scope.Stack.pop()
- length = scope.Stack.pop()
- )
+ dst, src, length := scope.Stack.pop3()
// These values are checked for overflow during memory expansion calculation
// (the memorySize function on the opcode).
scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64())
@@ -363,11 +359,8 @@ func enable8024(jt *JumpTable) {
func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
- stack = scope.Stack
- a = stack.pop()
- memOffset = stack.pop()
- codeOffset = stack.pop()
- length = stack.pop()
+ stack = scope.Stack
+ a, memOffset, codeOffset, length = stack.pop4()
)
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow {
@@ -377,7 +370,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
code := evm.StateDB.GetCode(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -403,7 +396,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// advanced past this boundary.
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: wanted}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -430,7 +423,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc {
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall {
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
- scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified)
+ scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -590,3 +583,11 @@ func enable7843(jt *JumpTable) {
maxStack: maxStack(0, 1),
}
}
+
+// enable8037 enables the multidimensional-metering as specified in EIP-8037.
+func enable8037(jt *JumpTable) {
+ jt[CREATE].constantGas = params.CreateGasAmsterdam
+ jt[CREATE2].constantGas = params.CreateGasAmsterdam
+ jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
+ jt[SSTORE].dynamicGas = gasSStore8037
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 832306b9a0..50d9e8ab0c 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -67,6 +67,8 @@ type BlockContext struct {
BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price)
Random *common.Hash // Provides information for PREVRANDAO
SlotNum uint64 // Provides information for SLOTNUM
+
+ CostPerStateByte uint64 // CostPerByte for new state after EIP-8037
}
// TxContext provides the EVM with information about a transaction.
@@ -245,13 +247,13 @@ func isSystemCall(caller common.Address) bool {
// parameters. It also handles any necessary value transfer required and takse
// the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer.
-func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Capture the tracer start/end events in debug mode
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -263,7 +265,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
- snapshot := evm.StateDB.Snapshot()
+ snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
p, isPrecompile := evm.precompile(addr)
if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
@@ -275,10 +277,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// hash leaf to the access list, then account creation will proceed unimpaired.
// Thus, only pay for the creation of the code hash leaf here.
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
- if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok {
+ if _, ok := gas.ChargeRegular(wgas); !ok {
evm.StateDB.RevertToSnapshot(snapshot)
- gas.Exhaust()
- return nil, gas, ErrOutOfGas
+ return nil, gas.ExitHalt(reservoir), ErrOutOfGas
}
}
@@ -288,6 +289,16 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
evm.StateDB.CreateAccount(addr)
}
+ if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
+ prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
+ if !ok {
+ evm.StateDB.RevertToSnapshot(snapshot)
+ return nil, gas.ExitHalt(reservoir), ErrOutOfGas
+ }
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
+ }
+ }
// Perform the value transfer only in non-syscall mode.
// Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied.
@@ -311,22 +322,19 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
gas = contract.Gas
}
}
- // When an error was returned by the EVM or when setting the creation code
- // above we revert to the snapshot and consume any gas remaining. Additionally,
- // when we're in homestead this also counts for code storage gas errors.
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err, reservoir)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
- // TODO: consider clearing up unused snapshots:
- //} else {
- // evm.StateDB.DiscardSnapshot(snapshot)
}
- return ret, gas, err
+ return ret, exitGas, err
}
// CallCode executes the contract associated with the addr with the given input
@@ -336,26 +344,23 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
//
// CallCode differs from Call in the sense that it executes the given address'
// code with the caller as context.
-func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// Fail if we're trying to transfer more than the available balance
- // Note although it's noop to transfer X ether to caller itself. But
- // if caller doesn't have enough balance, it would be an error to allow
- // over-charging itself. So the check here is necessary.
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
- var snapshot = evm.StateDB.Snapshot()
+ snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
@@ -368,16 +373,19 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err, reservoir)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// DelegateCall executes the contract associated with the addr with the given input
@@ -385,56 +393,56 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
//
// DelegateCall differs from CallCode in the sense that it executes the given address'
// code with the caller as context and the caller is set to the caller of the caller.
-func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
// DELEGATECALL inherits value from parent call
- evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
- var snapshot = evm.StateDB.Snapshot()
+ snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
- // Initialise a new contract and make initialise the delegate values
- //
- // Note: The value refers to the original value from the parent call.
contract := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err, reservoir)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
+
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// StaticCall executes the contract associated with the addr with the given input
// as parameters while disallowing any modifications to the state during the call.
// Opcodes that attempt to perform such modifications will result in exceptions
// instead of performing the modifications.
-func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, leftOverGas GasBudget, err error) {
+func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, result GasBudget, err error) {
// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil)
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
+ evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil)
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
@@ -445,7 +453,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// after all empty accounts were deleted, so this is not required. However, if we omit this,
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
// We could change this, but for now it's left for legacy reasons
- var snapshot = evm.StateDB.Snapshot()
+ snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
// We do an AddBalance of zero here, just in order to trigger a touch.
// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,
@@ -456,58 +464,59 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else {
- // Initialise a new contract and set the code that is to be used by the EVM.
- // The contract is a scoped environment for this execution context only.
contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
-
- // When an error was returned by the EVM or when setting the creation code
- // above we revert to the snapshot and consume any gas remaining. Additionally
- // when we're in Homestead this also counts for code storage gas errors.
ret, err = evm.Run(contract, input, true)
gas = contract.Gas
}
+
+ // Calculate the remaining gas at the end of frame
+ exitGas := gas.Exit(err, reservoir)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
}
}
- return ret, gas, err
+ return ret, exitGas, err
}
// create creates a new contract using code as deployment code.
-func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasBudget, err error) {
- if evm.Config.Tracer != nil {
- evm.captureBegin(evm.depth, typ, caller, address, code, gas.RegularGas, value.ToBig())
- defer func(startGas uint64) {
- evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err)
- }(gas.RegularGas)
- }
+func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) {
// Depth check execution. Fail if we're trying to execute above the
// limit.
+ var nonce uint64
if evm.depth > int(params.CallCreateDepth) {
- return nil, common.Address{}, gas, ErrDepth
+ err = ErrDepth
+ } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
+ err = ErrInsufficientBalance
+ } else {
+ nonce = evm.StateDB.GetNonce(caller)
+ if nonce+1 < nonce {
+ err = ErrNonceUintOverflow
+ }
}
- if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
- return nil, common.Address{}, gas, ErrInsufficientBalance
+ if evm.Config.Tracer != nil {
+ evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig())
+ defer func(startGas GasBudget) {
+ evm.captureEnd(evm.depth, startGas, result, ret, err)
+ }(gas)
}
- nonce := evm.StateDB.GetNonce(caller)
- if nonce+1 < nonce {
- return nil, common.Address{}, gas, ErrNonceUintOverflow
+ if err != nil {
+ return nil, common.Address{}, gas, err
}
+ // Increment the caller's nonce after passing all validations
evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
+ reservoir := gas.StateGas
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
if !ok {
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrOutOfGas
+ return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
@@ -528,11 +537,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
+ halt := gas.ExitHalt(reservoir)
if evm.Config.Tracer.HasGasHook() {
- evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
+ evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
}
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrContractAddressCollision
+ // EIP-8037 collision rule: the state reservoir is fully preserved on
+ // address collision while regular gas is burnt.
+ return nil, common.Address{}, halt, ErrContractAddressCollision
}
// Create a new account on the state only if the object was not present.
// It might be possible the contract code is deployed to a pre-existent
@@ -540,6 +551,18 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
snapshot := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(address)
+
+ if evm.chainRules.IsAmsterdam && evm.depth > 0 {
+ // Only charge state gas if we are not doing a create transaction.
+ // Prevents double charging with IntrinsicGas.
+ prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
+ if !ok {
+ return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
+ }
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
+ }
+ }
}
// CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account.
@@ -554,8 +577,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted {
- gas.Exhaust()
- return nil, common.Address{}, gas, ErrOutOfGas
+ return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() {
@@ -574,13 +596,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
contract.IsDeployment = true
ret, err = evm.initNewContract(contract, address)
+
+ // Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back
+ // state and gas is preserved (i.e., treated as success).
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
+
+ exit := contract.Gas.Exit(err, reservoir)
if err != ErrExecutionReverted {
- contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
+ if evm.Config.Tracer.HasGasHook() {
+ evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
+ }
}
+ return ret, address, exit, err
}
- return ret, address, contract.Gas, err
+ // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved).
+ // Both packaged as a success-form GasBudget.
+ return ret, address, contract.Gas.ExitSuccess(), err
}
// initNewContract runs a new contract's creation code, performs checks on the
@@ -590,30 +622,45 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
if err != nil {
return ret, err
}
-
- // Check whether the max code size has been exceeded, assign err if the case.
- if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
- return ret, err
- }
-
+ // Check prefix before gas calculation.
// Reject code starting with 0xEF if EIP-3541 is enabled.
if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
return ret, ErrInvalidCode
}
-
- if !evm.chainRules.IsEIP4762 {
- createDataGas := uint64(len(ret)) * params.CreateDataGas
- if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
- return ret, ErrCodeStoreOutOfGas
- }
- } else {
+ if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
- contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if len(ret) > 0 && (consumed < wanted) {
return ret, ErrCodeStoreOutOfGas
}
+ if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
+ return ret, err
+ }
+ } else if evm.chainRules.IsAmsterdam {
+ // Check max code size BEFORE charging gas so over-max code
+ // does not consume state gas (which would inflate tx_state).
+ if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
+ return ret, err
+ }
+ // Charge regular gas (hash cost) before state gas.
+ regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas
+ if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ return ret, ErrCodeStoreOutOfGas
+ }
+ // Charge state gas (code-deposit) afterwards.
+ stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte
+ if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ return ret, ErrCodeStoreOutOfGas
+ }
+ } else {
+ createDataCost := uint64(len(ret)) * params.CreateDataGas
+ if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
+ return ret, ErrCodeStoreOutOfGas
+ }
+ if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil {
+ return ret, err
+ }
}
-
if len(ret) > 0 {
evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation)
}
@@ -621,7 +668,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
}
// Create creates a new contract using code as deployment code.
-func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
+func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE)
}
@@ -630,7 +677,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value
//
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
-func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) {
+func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)
@@ -668,22 +715,20 @@ func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash {
// ChainConfig returns the environment's chain configuration
func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig }
-func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas uint64, value *big.Int) {
+func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas GasBudget, value *big.Int) {
tracer := evm.Config.Tracer
if tracer.OnEnter != nil {
- tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value)
+ tracer.OnEnter(depth, byte(typ), from, to, input, startGas.RegularGas, value)
}
if tracer.HasGasHook() {
- initial := NewGasBudget(startGas)
- tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance)
+ tracer.EmitGasChange(tracing.Gas{}, startGas.AsTracing(), tracing.GasChangeCallInitialBalance)
}
}
-func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) {
+func (evm *EVM) captureEnd(depth int, startGas GasBudget, leftOverGas GasBudget, ret []byte, err error) {
tracer := evm.Config.Tracer
- if leftOverGas != 0 && tracer.HasGasHook() {
- leftover := NewGasBudget(leftOverGas)
- tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
+ if !leftOverGas.IsZero() && tracer.HasGasHook() {
+ tracer.EmitGasChange(leftOverGas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
}
var reverted bool
if err != nil {
@@ -693,7 +738,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [
reverted = false
}
if tracer.OnExit != nil {
- tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted)
+ tracer.OnExit(depth, ret, startGas.RegularGas-leftOverGas.RegularGas, VMErrorFromErr(err), reverted)
}
}
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index 046311f9cc..550375c9c0 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -291,10 +291,19 @@ var (
gasMLoad = pureMemoryGascost
gasMStore8 = pureMemoryGascost
gasMStore = pureMemoryGascost
- gasCreate = pureMemoryGascost
)
+func gasCreate(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
+ return pureMemoryGascost(evm, contract, stack, mem, memorySize)
+}
+
func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@@ -313,6 +322,9 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS
}
func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@@ -331,7 +343,11 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
return GasCosts{RegularGas: gas}, nil
}
+
func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@@ -384,17 +400,17 @@ var (
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic)
)
-func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
+func makeCallVariantGasCost(intrinsicFunc intrinsicGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic, stack.back(0))
if err != nil {
return GasCosts{}, err
}
- gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp)
+ gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
@@ -402,19 +418,19 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
}
}
-func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.back(2).IsZero()
address = common.Address(stack.back(1).Bytes20())
)
if evm.readOnly && transfersValue {
- return GasCosts{}, ErrWriteProtection
+ return 0, ErrWriteProtection
}
// Stateless check
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
@@ -422,12 +438,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
if contract.Gas.RegularGas < gas {
- return GasCosts{}, ErrOutOfGas
+ return 0, ErrOutOfGas
}
// Stateful check
var stateGas uint64
@@ -439,15 +455,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
stateGas += params.CallNewAccountGas
}
if gas, overflow = math.SafeAdd(gas, stateGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
var (
gas uint64
@@ -457,38 +473,36 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor
gas += params.CallValueTransferGas
}
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
- return GasCosts{}, ErrGasUintOverflow
+ return 0, ErrGasUintOverflow
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
-func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
- return GasCosts{}, err
+ return 0, err
}
- return GasCosts{RegularGas: gas}, nil
+ return gas, nil
}
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
-
var gas uint64
// EIP150 homestead gas reprice fork:
if evm.chainRules.IsEIP150 {
gas = params.SelfdestructGasEIP150
var address = common.Address(stack.back(0).Bytes20())
-
if evm.chainRules.IsEIP158 {
// if empty and transfers value
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
@@ -504,3 +518,104 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
}
return GasCosts{RegularGas: gas}, nil
}
+
+func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ if evm.readOnly {
+ return GasCosts{}, ErrWriteProtection
+ }
+ var (
+ gas GasCosts
+ address = common.Address(stack.peek().Bytes20())
+ )
+ if !evm.StateDB.AddressInAccessList(address) {
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddAddressToAccessList(address)
+ gas.RegularGas = params.ColdAccountAccessCostEIP2929
+ }
+ // Check we have enough regular gas before we add the address to the BAL
+ if contract.Gas.RegularGas < gas.RegularGas {
+ return gas, ErrOutOfGas
+ }
+ // Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist
+ // in the current state yet still be considered non-existent by EIP-161 if its
+ // nonce, balance, and code are all zero. Such accounts can appear temporarily
+ // during execution (e.g. via SELFDESTRUCT) and are removed at tx end.
+ //
+ // 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.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte
+ }
+ return gas, nil
+}
+
+func gasSStore8037(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 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
+ )
+ // Check slot presence in the access list
+ 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
+ evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
+ }
+ value := common.Hash(y.Bytes32())
+
+ if current == value { // noop (1)
+ // EIP 2200 original clause:
+ // return params.SloadGasEIP2200, nil
+ return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS
+ }
+ if original == current {
+ if original == (common.Hash{}) { // create slot (2.1.1)
+ return GasCosts{
+ RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929,
+ StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte,
+ }, nil
+ }
+ if value == (common.Hash{}) { // delete slot (2.1.2b)
+ evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ }
+ // 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)
+ }
+ if original != (common.Hash{}) {
+ if current == (common.Hash{}) { // recreate slot (2.2.1.1)
+ evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529)
+ } else if value == (common.Hash{}) { // delete slot (2.2.1.2)
+ evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ }
+ }
+ if original == value {
+ 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)
+ }
+ }
+ // EIP-2200 original clause:
+ //return params.SloadGasEIP2200, nil // dirty update (2.2)
+ return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2)
+}
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index 16ce651a7d..4bf936d07d 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) {
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
- initialGas := NewGasBudget(tt.gaspool)
- _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
+ initialGas := NewGasBudget(tt.gaspool, 0)
+ _, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int))
if !errors.Is(err, tt.failure) {
t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure)
}
- if used := leftOver.Used(initialGas); used != tt.used {
+ if used := result.Used(initialGas); used != tt.used {
t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
}
if refund := evm.StateDB.GetRefund(); refund != tt.refund {
@@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) {
}
evm := NewEVM(vmctx, statedb, chainConfig, config)
- initialGas := NewGasBudget(uint64(testGas))
- ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int))
+ initialGas := NewGasBudget(uint64(testGas), 0)
+ ret, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int))
if err != nil {
return false
}
- gasUsed = leftOver.Used(initialGas)
+ gasUsed = result.Used(initialGas)
if len(ret) != 32 {
t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret))
}
diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go
index ed938ae41f..881575e0a8 100644
--- a/core/vm/gascosts.go
+++ b/core/vm/gascosts.go
@@ -20,11 +20,11 @@ import (
"fmt"
"github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/log"
)
-// GasCosts denotes a vector of gas costs in the
-// multidimensional metering paradigm. It represents the cost
-// charged by an individual operation.
+// GasCosts denotes a vector of gas costs in the multidimensional metering
+// paradigm. It represents the cost charged by an individual operation.
type GasCosts struct {
RegularGas uint64
StateGas uint64
@@ -40,67 +40,259 @@ func (g GasCosts) String() string {
return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
}
-// GasBudget denotes a vector of remaining gas allowances available
-// for EVM execution in the multidimensional metering paradigm.
-// Unlike GasCosts which represents the price of an operation,
-// GasBudget tracks how much gas is left to spend.
+// GasBudget is the unified gas-state structure used throughout the EVM.
+// It carries two pairs of fields:
+//
+// - RegularGas / StateGas: the running balance during execution, or the
+// leftover balance the caller must absorb after a sub-call.
+// - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross
+// consumption. UsedStateGas is signed so it can be decremented by inline
+// state-gas refunds (e.g., SSTORE 0->A->0).
+//
+// The same struct serves three roles:
+//
+// - During execution: Charge / ChargeRegular / ChargeState / RefundState
+// and RefundRegular mutate the running balance and the usage accumulators
+// in lockstep.
+//
+// - At frame exit: ExitSuccess / ExitRevert / ExitHalt produce a new
+// GasBudget in "leftover" form that packages the result for the caller.
+//
+// - At absorption: the caller's Absorb method merges the child's leftover
+// budget into its own running budget.
type GasBudget struct {
- RegularGas uint64 // The leftover gas for execution and state gas usage
- StateGas uint64 // The state gas reservoir
+ RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb)
+ StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb)
+ UsedRegularGas uint64 // gross regular gas consumed in this frame
+ UsedStateGas int64 // signed net state-gas consumed in this frame
}
-// NewGasBudget creates a GasBudget with the given initial regular gas allowance.
-func NewGasBudget(gas uint64) GasBudget {
- return GasBudget{RegularGas: gas}
+// NewGasBudget initializes a fresh GasBudget for execution / forwarding,
+// with both usage accumulators set to zero.
+func NewGasBudget(regular, state uint64) GasBudget {
+ return GasBudget{RegularGas: regular, StateGas: state}
}
-// Used returns the amount of regular gas consumed so far.
+// Used returns the total scalar gas consumed relative to an initial budget
+// (= (initial.regular + initial.state) − (current.regular + current.state)).
+// This is the payment scalar (EIP-8037's tx_gas_used_before_refund).
func (g GasBudget) Used(initial GasBudget) uint64 {
- return initial.RegularGas - g.RegularGas
+ return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas)
}
-// Exhaust sets all remaining gas to zero, preserving the initial amount
-// for usage tracking.
-func (g *GasBudget) Exhaust() {
- g.RegularGas = 0
- g.StateGas = 0
-}
-
-func (g *GasBudget) Copy() GasBudget {
- return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas}
-}
-
-// String returns a visual representation of the gas budget vector.
+// String returns a visual representation of the budget.
func (g GasBudget) String() string {
- return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
+ return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas)
}
-// CanAfford reports whether the budget has sufficient gas to cover the cost.
-func (g GasBudget) CanAfford(cost GasCosts) bool {
- return g.RegularGas >= cost.RegularGas
-}
-
-// Charge deducts the given gas cost from the budget. It returns the
-// pre-charge budget and false if the budget does not have sufficient
-// gas to cover the cost.
+// Charge deducts a combined regular+state cost from the running balance and
+// updates the usage accumulators. State-gas in excess of the reservoir spills
+// into regular_gas.
func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
prior := *g
- if g.RegularGas < cost.RegularGas {
- return prior, false
- }
- g.RegularGas -= cost.RegularGas
- return prior, true
+ ok := g.charge(cost)
+ return prior, ok
}
-// Refund adds the given gas budget back. It returns the pre-refund budget
-// and whether the budget was actually changed.
-func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) {
- prior := *g
- g.RegularGas += other.RegularGas
- return prior, g.RegularGas != prior.RegularGas
+// chargeRegularOnly deducts a regular-only cost.
+func (g *GasBudget) chargeRegularOnly(r uint64) bool {
+ if g.RegularGas < r {
+ return false
+ }
+ g.RegularGas -= r
+ g.UsedRegularGas += r
+ return true
+}
+
+// charge deducts both the state and regular cost.
+func (g *GasBudget) charge(cost GasCosts) bool {
+ if g.RegularGas < cost.RegularGas {
+ return false
+ }
+ regular := g.RegularGas - cost.RegularGas
+ state := g.StateGas
+
+ if cost.StateGas > state {
+ spillover := cost.StateGas - state
+ if spillover > regular {
+ return false
+ }
+ regular -= spillover
+ state = 0
+ } else {
+ state -= cost.StateGas
+ }
+ g.RegularGas = regular
+ g.StateGas = state
+ g.UsedRegularGas += cost.RegularGas
+ g.UsedStateGas += int64(cost.StateGas)
+ return true
}
// AsTracing converts the GasBudget into the tracing-facing Gas vector.
func (g GasBudget) AsTracing() tracing.Gas {
return tracing.Gas{Regular: g.RegularGas, State: g.StateGas}
}
+
+// ChargeRegular is a convenience that deducts a regular-only cost.
+func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) {
+ return g.Charge(GasCosts{RegularGas: r})
+}
+
+// ChargeState is a convenience that deducts a state-only cost (spills to
+// regular when the reservoir is exhausted). Returns false on OOG.
+func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) {
+ return g.Charge(GasCosts{StateGas: s})
+}
+
+// IsZero returns an indicator if the gas budget has been exhausted.
+func (g *GasBudget) IsZero() bool {
+ return g.RegularGas == 0 && g.StateGas == 0
+}
+
+// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0).
+// The reservoir is credited and the signed usage counter is decremented
+// in lockstep, preserving the per-frame invariant:
+//
+// StateGas + UsedStateGas == initialStateGas + spillover_so_far
+//
+// which the revert path relies on for the correct gross refund.
+func (g *GasBudget) RefundState(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
+// that the absorb-on-return path correctly reclaims the unused portion.
+//
+// Used by frame boundaries where the regular forward has NOT been pre-
+// deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The
+// CALL family pre-deducts the forward via the dynamic gas table for tracer-
+// reporting reasons and therefore constructs its child budget directly.
+//
+// Caller must ensure `regular` does not exceed the running balance and
+// apply any EIP-150 1/64 retention before calling Forward.
+func (g *GasBudget) Forward(regular uint64) GasBudget {
+ g.RegularGas -= regular
+ g.UsedRegularGas += regular
+
+ child := GasBudget{
+ RegularGas: regular,
+ StateGas: g.StateGas,
+ }
+ g.StateGas = 0
+ return child
+}
+
+// ForwardAll forwards the parent's full remaining budget (both regular and
+// state) to a child frame. Equivalent to Forward(g.RegularGas) — used at
+// the tx boundary where there is no 1/64 retention.
+func (g *GasBudget) ForwardAll() GasBudget {
+ return g.Forward(g.RegularGas)
+}
+
+// ============================================================================
+// Exit-form constructors. These take a post-execution running budget and
+// produce a new GasBudget in "leftover form", the value the caller should
+// absorb to update its own state.
+// ============================================================================
+
+// ExitSuccess produces the leftover form for a successful frame. Inline
+// state-gas refunds have already been folded into StateGas / UsedStateGas
+// during execution; the running budget IS the exit budget on success.
+func (g GasBudget) ExitSuccess() GasBudget {
+ return g
+}
+
+// ExitRevert produces the leftover for a REVERT exit. Per EIP-8037, all state
+// gas charged by the reverted frame is refunded to the caller's reservoir:
+//
+// leftover.StateGas = StateGas + UsedStateGas
+//
+// UsedStateGas is reset since the frame's state changes are discarded.
+func (g GasBudget) ExitRevert() GasBudget {
+ reservoir := int64(g.StateGas) + g.UsedStateGas
+ if reservoir < 0 {
+ // Reservoir should never be negative. By construction it equals
+ // the initial state-gas allocation plus any spillover to regular
+ // gas.
+ reservoir = 0
+ log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas)
+ }
+ return GasBudget{
+ RegularGas: g.RegularGas,
+ StateGas: uint64(reservoir),
+ UsedRegularGas: g.UsedRegularGas,
+ UsedStateGas: 0,
+ }
+}
+
+// ExitHalt produces the leftover for an exceptional halt.
+//
+// - state_gas_reservoir is reset back to its value at the start of the child frame
+// - the gas_left initially given to the child is consumed (set to zero)
+func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
+ reservoir := int64(g.StateGas) + g.UsedStateGas
+ if reservoir < 0 {
+ // Reservoir should never be negative. By construction it equals
+ // the initial state-gas allocation plus any spillover to regular
+ // gas.
+ reservoir = 0
+ log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas)
+ }
+ // The portion of state gas charged from regular gas is also burned
+ // together with the regular gas, rather than being returned to the
+ // parent's state-gas reservoir.
+ var spilled uint64
+ if uint64(reservoir) > initStateReservoir {
+ spilled = uint64(reservoir) - initStateReservoir
+ }
+ return GasBudget{
+ RegularGas: 0,
+ StateGas: initStateReservoir,
+ UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled,
+ UsedStateGas: 0,
+ }
+}
+
+// Exit dispatches on err to the appropriate exit-form constructor
+// for the post-evm.Run path:
+//
+// - err == nil → ExitSuccess
+// - err == ErrExecutionReverted → ExitRevert
+// - any other err → ExitHalt
+//
+// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
+// directly instead of going through this dispatcher.
+func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
+ switch {
+ case err == nil:
+ return g.ExitSuccess()
+ case err == ErrExecutionReverted:
+ return g.ExitRevert()
+ default:
+ return g.ExitHalt(initStateReservoir)
+ }
+}
+
+// Absorb merges a sub-call's leftover GasBudget into this (caller's) running
+// budget. Additionally, it does an EIP-8037 spillover correction:
+// state-gas that spilled into the regular pool inside the child frame is
+// excluded from the UsedRegularGas.
+//
+// spillover = forwarded - child.RegularGas - child.UsedRegularGas
+//
+// forwarded is the regular-gas amount that was passed to the child at call
+// entry (i.e., the regular initial of the child's GasBudget).
+func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) {
+ spillover := forwarded - child.RegularGas - child.UsedRegularGas
+
+ g.UsedRegularGas -= child.RegularGas
+ g.RegularGas += child.RegularGas
+ g.StateGas = child.StateGas
+ g.UsedStateGas += child.UsedStateGas
+
+ g.UsedRegularGas -= spillover
+}
diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index 4b05092cc7..209457f670 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -27,56 +27,56 @@ import (
)
func opAdd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Add(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Add(x, y)
return nil, nil
}
func opSub(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Sub(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Sub(x, y)
return nil, nil
}
func opMul(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Mul(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Mul(x, y)
return nil, nil
}
func opDiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Div(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Div(x, y)
return nil, nil
}
func opSdiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.SDiv(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.SDiv(x, y)
return nil, nil
}
func opMod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Mod(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Mod(x, y)
return nil, nil
}
func opSmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.SMod(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.SMod(x, y)
return nil, nil
}
func opExp(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- base, exponent := scope.Stack.pop(), scope.Stack.peek()
- exponent.Exp(&base, exponent)
+ base, exponent := scope.Stack.pop1Peek1()
+ exponent.Exp(base, exponent)
return nil, nil
}
func opSignExtend(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- back, num := scope.Stack.pop(), scope.Stack.peek()
- num.ExtendSign(num, &back)
+ back, num := scope.Stack.pop1Peek1()
+ num.ExtendSign(num, back)
return nil, nil
}
@@ -87,7 +87,7 @@ func opNot(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opLt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Lt(y) {
y.SetOne()
} else {
@@ -97,7 +97,7 @@ func opLt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opGt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Gt(y) {
y.SetOne()
} else {
@@ -107,7 +107,7 @@ func opGt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opSlt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Slt(y) {
y.SetOne()
} else {
@@ -117,7 +117,7 @@ func opSlt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opSgt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Sgt(y) {
y.SetOne()
} else {
@@ -127,7 +127,7 @@ func opSgt(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opEq(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
+ x, y := scope.Stack.pop1Peek1()
if x.Eq(y) {
y.SetOne()
} else {
@@ -147,38 +147,38 @@ func opIszero(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opAnd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.And(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.And(x, y)
return nil, nil
}
func opOr(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Or(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Or(x, y)
return nil, nil
}
func opXor(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y := scope.Stack.pop(), scope.Stack.peek()
- y.Xor(&x, y)
+ x, y := scope.Stack.pop1Peek1()
+ y.Xor(x, y)
return nil, nil
}
func opByte(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- th, val := scope.Stack.pop(), scope.Stack.peek()
- val.Byte(&th)
+ th, val := scope.Stack.pop1Peek1()
+ val.Byte(th)
return nil, nil
}
func opAddmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
- z.AddMod(&x, &y, z)
+ x, y, z := scope.Stack.pop2Peek1()
+ z.AddMod(x, y, z)
return nil, nil
}
func opMulmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
- z.MulMod(&x, &y, z)
+ x, y, z := scope.Stack.pop2Peek1()
+ z.MulMod(x, y, z)
return nil, nil
}
@@ -187,7 +187,7 @@ func opMulmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// and pushes on the stack arg2 shifted to the left by arg1 number of bits.
func opSHL(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Note, second operand is left in the stack; accumulate result into it, and no need to push it afterwards
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.LtUint64(256) {
value.Lsh(value, uint(shift.Uint64()))
} else {
@@ -201,7 +201,7 @@ func opSHL(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// and pushes on the stack arg2 shifted to the right by arg1 number of bits with zero fill.
func opSHR(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Note, second operand is left in the stack; accumulate result into it, and no need to push it afterwards
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.LtUint64(256) {
value.Rsh(value, uint(shift.Uint64()))
} else {
@@ -214,7 +214,7 @@ func opSHR(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// The SAR instruction (arithmetic shift right) pops 2 values from the stack, first arg1 and then arg2,
// and pushes on the stack arg2 shifted to the right by arg1 number of bits with sign extension.
func opSAR(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- shift, value := scope.Stack.pop(), scope.Stack.peek()
+ shift, value := scope.Stack.pop1Peek1()
if shift.GtUint64(256) {
if value.Sign() >= 0 {
value.Clear()
@@ -230,7 +230,7 @@ func opSAR(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opKeccak256(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- offset, size := scope.Stack.pop(), scope.Stack.peek()
+ offset, size := scope.Stack.pop1Peek1()
data := scope.Memory.GetPtr(offset.Uint64(), size.Uint64())
hash := crypto.Keccak256Hash(data)
@@ -286,11 +286,7 @@ func opCallDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCallDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- memOffset = scope.Stack.pop()
- dataOffset = scope.Stack.pop()
- length = scope.Stack.pop()
- )
+ memOffset, dataOffset, length := scope.Stack.pop3()
dataOffset64, overflow := dataOffset.Uint64WithOverflow()
if overflow {
dataOffset64 = math.MaxUint64
@@ -309,11 +305,7 @@ func opReturnDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error)
}
func opReturnDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- memOffset = scope.Stack.pop()
- dataOffset = scope.Stack.pop()
- length = scope.Stack.pop()
- )
+ memOffset, dataOffset, length := scope.Stack.pop3()
offset64, overflow := dataOffset.Uint64WithOverflow()
if overflow {
@@ -321,7 +313,7 @@ func opReturnDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error)
}
// we can reuse dataOffset now (aliasing it for clarity)
var end = dataOffset
- end.Add(&dataOffset, &length)
+ end.Add(dataOffset, length)
end64, overflow := end.Uint64WithOverflow()
if overflow || uint64(len(evm.returnData)) < end64 {
return nil, ErrReturnDataOutOfBounds
@@ -342,11 +334,7 @@ func opCodeSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- var (
- memOffset = scope.Stack.pop()
- codeOffset = scope.Stack.pop()
- length = scope.Stack.pop()
- )
+ memOffset, codeOffset, length := scope.Stack.pop3()
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow {
uint64CodeOffset = math.MaxUint64
@@ -359,11 +347,8 @@ func opCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
func opExtCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
- stack = scope.Stack
- a = stack.pop()
- memOffset = stack.pop()
- codeOffset = stack.pop()
- length = stack.pop()
+ stack = scope.Stack
+ a, memOffset, codeOffset, length = stack.pop4()
)
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow {
@@ -480,7 +465,7 @@ func opGasLimit(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opPop(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- scope.Stack.pop()
+ scope.Stack.drop()
return nil, nil
}
@@ -492,13 +477,13 @@ func opMload(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opMstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- mStart, val := scope.Stack.pop(), scope.Stack.pop()
- scope.Memory.Set32(mStart.Uint64(), &val)
+ mStart, val := scope.Stack.pop2()
+ scope.Memory.Set32(mStart.Uint64(), val)
return nil, nil
}
func opMstore8(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- off, val := scope.Stack.pop(), scope.Stack.pop()
+ off, val := scope.Stack.pop2()
scope.Memory.store[off.Uint64()] = byte(val.Uint64())
return nil, nil
}
@@ -515,8 +500,7 @@ func opSstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.readOnly {
return nil, ErrWriteProtection
}
- loc := scope.Stack.pop()
- val := scope.Stack.pop()
+ loc, val := scope.Stack.pop2()
evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32())
return nil, nil
}
@@ -525,8 +509,8 @@ func opJump(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.abort.Load() {
return nil, errStopToken
}
- pos := scope.Stack.pop()
- if !scope.Contract.validJumpdest(&pos) {
+ pos := scope.Stack.pop1()
+ if !scope.Contract.validJumpdest(pos) {
return nil, ErrInvalidJump
}
*pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop
@@ -537,9 +521,9 @@ func opJumpi(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.abort.Load() {
return nil, errStopToken
}
- pos, cond := scope.Stack.pop(), scope.Stack.pop()
+ pos, cond := scope.Stack.pop2()
if !cond.IsZero() {
- if !scope.Contract.validJumpdest(&pos) {
+ if !scope.Contract.validJumpdest(pos) {
return nil, ErrInvalidJump
}
*pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop
@@ -647,25 +631,22 @@ func opSwap16(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- if evm.readOnly {
- return nil, ErrWriteProtection
- }
var (
value = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
- gas = scope.Contract.Gas.RegularGas
)
+ // Apply EIP-150 to the regular gas left after the state charge.
+ forward := scope.Contract.Gas.RegularGas
if evm.chainRules.IsEIP150 {
- gas -= gas / 64
+ forward -= forward / 64
}
// reuse size int for stackvalue
stackvalue := size
- scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
-
- res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value)
+ child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
+ res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
// Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must
@@ -679,7 +660,8 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
scope.Stack.push(&stackvalue)
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ // Refund the leftover gas back to current frame
+ scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
@@ -690,24 +672,20 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- if evm.readOnly {
- return nil, ErrWriteProtection
- }
var (
endowment = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop()
salt = scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
- gas = scope.Contract.Gas.RegularGas
)
+ // Apply EIP-150 to the regular gas left after the state charge.
+ forward := scope.Contract.Gas.RegularGas
+ forward -= forward / 64
- // Apply EIP150
- gas -= gas / 64
- scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
// reuse size int for stackvalue
stackvalue := size
- res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas),
- &endowment, &salt)
+ child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
+ res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
// Push item on the stack based on the returned error.
if suberr != nil {
stackvalue.Clear()
@@ -715,7 +693,9 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
stackvalue.SetBytes(addr.Bytes())
}
scope.Stack.push(&stackvalue)
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+
+ // Refund the leftover gas back to current frame
+ scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
@@ -743,7 +723,12 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() {
gas += params.CallStipend
}
- ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
+
+ // Regular gas for the forward was already pre-deducted by the dynamic
+ // gas table (see makeCallVariantGasCallEIP*); only the state reservoir
+ // needs to be handed off to the child here.
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.Call(scope.Contract.Address(), toAddr, args, childBudget, &value)
if err != nil {
temp.Clear()
@@ -751,11 +736,11 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
temp.SetOne()
}
stack.push(&temp)
+
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
-
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@@ -776,8 +761,11 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() {
gas += params.CallStipend
}
-
- ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value)
+ // Regular gas for the forward was already pre-deducted by the dynamic
+ // gas table, only the state reservoir needs to be handed off to the
+ // child here.
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value)
if err != nil {
temp.Clear()
} else {
@@ -788,7 +776,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@@ -806,7 +794,11 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value)
+ // Regular gas for the forward was already pre-deducted by the dynamic
+ // gas table, only the state reservoir needs to be handed off to the
+ // child here.
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value)
if err != nil {
temp.Clear()
} else {
@@ -816,8 +808,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
-
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@@ -835,7 +826,11 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
- ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas))
+ // Regular gas for the forward was already pre-deducted by the dynamic
+ // gas table, only the state reservoir needs to be handed off to the
+ // child here.
+ childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas)
+ ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget)
if err != nil {
temp.Clear()
} else {
@@ -846,21 +841,21 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
- scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
+ scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
}
func opReturn(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- offset, size := scope.Stack.pop(), scope.Stack.pop()
+ offset, size := scope.Stack.pop2()
ret := scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
return ret, errStopToken
}
func opRevert(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
- offset, size := scope.Stack.pop(), scope.Stack.pop()
+ offset, size := scope.Stack.pop2()
ret := scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
evm.returnData = ret
@@ -882,7 +877,7 @@ func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
this = scope.Contract.Address()
balance = evm.StateDB.GetBalance(this)
- top = scope.Stack.pop()
+ top = scope.Stack.pop1()
beneficiary = common.Address(top.Bytes20())
)
// The funds are burned immediately if the beneficiary is the caller itself,
@@ -912,7 +907,7 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
var (
this = scope.Contract.Address()
balance = evm.StateDB.GetBalance(this)
- top = scope.Stack.pop()
+ top = scope.Stack.pop1()
beneficiary = common.Address(top.Bytes20())
newContract = evm.StateDB.IsNewContract(this)
)
@@ -1081,9 +1076,9 @@ func makeLog(size int) executionFunc {
}
topics := make([]common.Hash, size)
stack := scope.Stack
- mStart, mSize := stack.pop(), stack.pop()
+ mStart, mSize := stack.pop2()
for i := 0; i < size; i++ {
- addr := stack.pop()
+ addr := stack.pop1()
topics[i] = addr.Bytes32()
}
diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go
index 354d2ce5ab..a143b7d01a 100644
--- a/core/vm/instructions_test.go
+++ b/core/vm/instructions_test.go
@@ -724,7 +724,7 @@ func TestRandom(t *testing.T) {
)
opRandom(&pc, evm, &ScopeContext{nil, stack, nil})
if have, want := stack.len(), 1; have != want {
- t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want)
+ t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, want, have)
}
actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes()))
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 3994327247..df69c3d1fd 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// associated costs.
contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
- contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
+ contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
}
@@ -192,10 +192,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
- if contract.Gas.RegularGas < cost {
+ if !contract.Gas.chargeRegularOnly(cost) {
return nil, ErrOutOfGas
- } else {
- contract.Gas.RegularGas -= cost
}
// All ops with a dynamic memory usage also has a dynamic gas cost.
@@ -224,11 +222,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
- // for tracing: this gas consumption event is emitted below in the debug section.
- if contract.Gas.RegularGas < dynamicCost.RegularGas {
+ if dynamicCost.StateGas == 0 {
+ if !contract.Gas.chargeRegularOnly(dynamicCost.RegularGas) {
+ return nil, ErrOutOfGas
+ }
+ } else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas
- } else {
- contract.Gas.RegularGas -= dynamicCost.RegularGas
}
}
diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go
index 868cb12d04..42530b83b7 100644
--- a/core/vm/interpreter_test.go
+++ b/core/vm/interpreter_test.go
@@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) {
timeout := make(chan bool)
go func(evm *EVM) {
- _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int))
+ _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64, 0), new(uint256.Int))
errChannel <- err
}(evm)
@@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) {
value = uint256.NewInt(0)
stack = newStackForTesting()
mem = NewMemory()
- contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil)
+ contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas, 0), nil)
)
stack.push(uint256.NewInt(123))
stack.push(uint256.NewInt(123))
diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go
index 82fc43ec13..9ea8349e3a 100644
--- a/core/vm/jump_table.go
+++ b/core/vm/jump_table.go
@@ -23,8 +23,9 @@ import (
)
type (
- executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error)
- gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64
+ executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error)
+ gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64
+ intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool)
)
@@ -97,6 +98,7 @@ 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)
return validate(instructionSet)
}
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index 86ac262a93..2206fb95fa 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
// gas for call
- if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -276,7 +276,7 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
-func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
+func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
@@ -295,7 +295,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Charge the remaining difference here already, to correctly calculate
// available gas for call
- if !contract.UseGas(GasCosts{RegularGas: eip2929Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@@ -312,7 +312,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) 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.RegularGas {
+ if contract.Gas.RegularGas < intrinsicCost {
return GasCosts{}, ErrOutOfGas
}
@@ -324,13 +324,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = params.ColdAccountAccessCostEIP2929
}
- if !contract.UseGas(GasCosts{RegularGas: eip7702Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+ if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied.
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0))
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost, stack.back(0))
if err != nil {
return GasCosts{}, err
}
@@ -339,6 +339,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// part of the dynamic gas. This will ensure it is correctly reported to
// tracers.
contract.Gas.RegularGas += eip2929Cost + eip7702Cost
+ // 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
// Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
// the CALL opcode itself, and the cost incurred by nested calls.
@@ -349,7 +353,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
- if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow {
+ if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
diff --git a/core/vm/runtime/env.go b/core/vm/runtime/env.go
index 4181e08ee8..de3e8d4ae2 100644
--- a/core/vm/runtime/env.go
+++ b/core/vm/runtime/env.go
@@ -19,6 +19,7 @@ package runtime
import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
@@ -29,17 +30,18 @@ func NewEnv(cfg *Config) *vm.EVM {
BlobHashes: cfg.BlobHashes,
}
blockContext := vm.BlockContext{
- CanTransfer: core.CanTransfer,
- Transfer: core.Transfer,
- GetHash: cfg.GetHashFn,
- Coinbase: cfg.Coinbase,
- BlockNumber: cfg.BlockNumber,
- Time: cfg.Time,
- Difficulty: cfg.Difficulty,
- GasLimit: cfg.GasLimit,
- BaseFee: cfg.BaseFee,
- BlobBaseFee: cfg.BlobBaseFee,
- Random: cfg.Random,
+ CanTransfer: core.CanTransfer,
+ Transfer: core.Transfer,
+ GetHash: cfg.GetHashFn,
+ Coinbase: cfg.Coinbase,
+ BlockNumber: cfg.BlockNumber,
+ Time: cfg.Time,
+ Difficulty: cfg.Difficulty,
+ GasLimit: cfg.GasLimit,
+ BaseFee: cfg.BaseFee,
+ BlobBaseFee: cfg.BlobBaseFee,
+ Random: cfg.Random,
+ CostPerStateByte: params.CostPerStateByte,
}
evm := vm.NewEVM(blockContext, cfg.State, cfg.ChainConfig, cfg.EVMConfig)
diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go
index 4fafdf3a50..34dec1bd4a 100644
--- a/core/vm/runtime/runtime.go
+++ b/core/vm/runtime/runtime.go
@@ -144,15 +144,15 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
// set the receiver's (the executing contract) code for execution.
cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified)
// Call the code with the given configuration.
- ret, leftOverGas, err := vmenv.Call(
+ ret, result, err := vmenv.Call(
cfg.Origin,
common.BytesToAddress([]byte("contract")),
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
return ret, cfg.State, err
}
@@ -179,16 +179,16 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
// - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration.
- code, address, leftOverGas, err := vmenv.Create(
+ code, address, result, err := vmenv.Create(
cfg.Origin,
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
- return code, address, leftOverGas.RegularGas, err
+ return code, address, result.RegularGas, err
}
// Call executes the code given by the contract's address. It will return the
@@ -213,15 +213,15 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er
statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration.
- ret, leftOverGas, err := vmenv.Call(
+ ret, result, err := vmenv.Call(
cfg.Origin,
address,
input,
- vm.NewGasBudget(cfg.GasLimit),
+ vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value),
)
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil {
- cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err)
+ cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err)
}
- return ret, leftOverGas.RegularGas, err
+ return ret, result.RegularGas, err
}
diff --git a/core/vm/stack.go b/core/vm/stack.go
index d8000bc86d..564345ccd8 100644
--- a/core/vm/stack.go
+++ b/core/vm/stack.go
@@ -121,6 +121,62 @@ func (s *Stack) len() int {
return s.size
}
+// drop removes the top element without reading it.
+func (s *Stack) drop() {
+ s.inner.top--
+ s.size--
+}
+
+// pop1 removes the top element and returns a pointer to it. The pointer
+// stays valid only until the next push or sub call.
+func (s *Stack) pop1() *uint256.Int {
+ s.inner.top--
+ s.size--
+ return &s.inner.data[s.inner.top]
+}
+
+// pop2 removes the top two elements and returns pointers to them. The
+// pointers stay valid only until the next push or sub call.
+func (s *Stack) pop2() (top, second *uint256.Int) {
+ s.inner.top -= 2
+ s.size -= 2
+ return &s.inner.data[s.inner.top+1], &s.inner.data[s.inner.top]
+}
+
+// pop3 removes the top three elements and returns pointers to them. The
+// pointers stay valid only until the next push or sub call.
+func (s *Stack) pop3() (top, second, third *uint256.Int) {
+ s.inner.top -= 3
+ s.size -= 3
+ return &s.inner.data[s.inner.top+2], &s.inner.data[s.inner.top+1], &s.inner.data[s.inner.top]
+}
+
+// pop4 removes the top four elements and returns pointers to them. The
+// pointers stay valid only until the next push or sub call.
+func (s *Stack) pop4() (top, second, third, fourth *uint256.Int) {
+ s.inner.top -= 4
+ s.size -= 4
+ return &s.inner.data[s.inner.top+3], &s.inner.data[s.inner.top+2], &s.inner.data[s.inner.top+1], &s.inner.data[s.inner.top]
+}
+
+// pop1Peek1 removes the top element and returns pointers to it and to the new
+// top, the usual operand and write target of a binary operation. The first
+// pointer stays valid only until the next push or sub call.
+func (s *Stack) pop1Peek1() (top, rest *uint256.Int) {
+ s.inner.top--
+ s.size--
+ return &s.inner.data[s.inner.top], &s.inner.data[s.inner.top-1]
+}
+
+// pop2Peek1 removes the top two elements and returns pointers to them and to
+// the new top, for three operand operations. The first two pointers stay
+// valid only until the next push or sub call.
+func (s *Stack) pop2Peek1() (top, second, rest *uint256.Int) {
+ s.inner.top -= 2
+ s.size -= 2
+ return &s.inner.data[s.inner.top+1], &s.inner.data[s.inner.top], &s.inner.data[s.inner.top-1]
+}
+
func (s *Stack) swap1() {
s.inner.data[s.bottom+s.size-2], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-2]
}
diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go
index 78098296e6..eb11a54e08 100644
--- a/eth/catalyst/api_test.go
+++ b/eth/catalyst/api_test.go
@@ -1936,111 +1936,6 @@ func newGetBlobEnv(t testing.TB, version byte, custody types.CustodyBitmap) (*no
api := newConsensusAPIWithoutHeartbeat(ethServ)
return n, api
}
-
-func TestGetBlobsV1(t *testing.T) {
- n, api := newGetBlobEnv(t, 0, types.CustodyBitmapAll)
- defer n.Close()
-
- suites := []struct {
- start int
- limit int
- fillRandom bool
- }{
- {
- start: 0, limit: 1,
- },
- {
- start: 0, limit: 1, fillRandom: true,
- },
- {
- start: 0, limit: 2,
- },
- {
- start: 0, limit: 2, fillRandom: true,
- },
- {
- start: 1, limit: 3,
- },
- {
- start: 1, limit: 3, fillRandom: true,
- },
- {
- start: 0, limit: 6,
- },
- {
- start: 0, limit: 6, fillRandom: true,
- },
- {
- start: 1, limit: 5,
- },
- {
- start: 1, limit: 5, fillRandom: true,
- },
- }
- for i, suite := range suites {
- // Fill the request for retrieving blobs
- var (
- vhashes []common.Hash
- expect engine.BlobAndProofListV1
- )
- // fill missing blob at the beginning
- if suite.fillRandom {
- vhashes = append(vhashes, testrand.Hash())
- expect = append(expect, nil)
- }
- for j := suite.start; j < suite.limit; j++ {
- vhashes = append(vhashes, testBlobVHashes[j])
- expect = append(expect, &engine.BlobAndProofV1{
- Blob: testBlobs[j][:],
- Proof: testBlobProofs[j][:],
- })
-
- // fill missing blobs in the middle
- if suite.fillRandom && rand.Intn(2) == 0 {
- vhashes = append(vhashes, testrand.Hash())
- expect = append(expect, nil)
- }
- }
- // fill missing blobs at the end
- if suite.fillRandom {
- vhashes = append(vhashes, testrand.Hash())
- expect = append(expect, nil)
- }
- result, err := api.GetBlobsV1(context.Background(), vhashes)
- if err != nil {
- t.Errorf("Unexpected error for case %d, %v", i, err)
- }
- if !reflect.DeepEqual(result, expect) {
- t.Fatalf("Unexpected result for case %d", i)
- }
- }
-}
-
-func TestGetBlobsV1AfterOsakaFork(t *testing.T) {
- genesis := &core.Genesis{
- Config: params.MergedTestChainConfig,
- Alloc: types.GenesisAlloc{testAddr: {Balance: testBalance}},
- Difficulty: common.Big0,
- Timestamp: 1, // Timestamp > 0 to ensure Osaka fork is active
- }
- n, ethServ := startEthService(t, genesis, nil)
- defer n.Close()
-
- var engineErr *engine.EngineAPIError
- api := newConsensusAPIWithoutHeartbeat(ethServ)
- _, err := api.GetBlobsV1(context.Background(), []common.Hash{testrand.Hash()})
- if !errors.As(err, &engineErr) {
- t.Fatalf("Unexpected error: %T", err)
- } else {
- if engineErr.ErrorCode() != -38005 {
- t.Fatalf("Expected error code -38005, got %d", engineErr.ErrorCode())
- }
- if engineErr.Error() != "Unsupported fork" {
- t.Fatalf("Expected error message 'Unsupported fork', got '%s'", engineErr.Error())
- }
- }
-}
-
func TestGetBlobsV2And3(t *testing.T) {
n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll)
defer n.Close()
diff --git a/eth/catalyst/api_testing.go b/eth/catalyst/api_testing.go
index 2818d7f0bb..b017641a5b 100644
--- a/eth/catalyst/api_testing.go
+++ b/eth/catalyst/api_testing.go
@@ -17,6 +17,7 @@
package catalyst
import (
+ "context"
"errors"
"github.com/ethereum/go-ethereum/beacon/engine"
@@ -47,6 +48,31 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
if api.eth.BlockChain().CurrentBlock().Hash() != parentHash {
return nil, errors.New("parentHash is not current head")
}
+ _, block, err := api.buildTestingBlock(payloadAttributes, transactions, extraData)
+ return block, err
+}
+
+// CommitBlockV1 builds a block from the supplied attributes and transactions, inserts
+// it into the chain, and sets it as the new canonical head. It is the equivalent of
+// BuildBlockV1 followed by engine_newPayload + engine_forkchoiceUpdated, but skips the
+// serialize/deserialize round-trip through ExecutableData. The block is built on top of
+// the current head.
+func (api *testingAPI) CommitBlockV1(ctx context.Context, payloadAttributes engine.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (common.Hash, error) {
+ block, _, err := api.buildTestingBlock(payloadAttributes, transactions, extraData)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ if _, err := api.eth.BlockChain().InsertBlockWithoutSetHead(ctx, block, false); err != nil {
+ return common.Hash{}, err
+ }
+ if _, err := api.eth.BlockChain().SetCanonical(block); err != nil {
+ return common.Hash{}, err
+ }
+ return block.Hash(), nil
+}
+
+func (api *testingAPI) buildTestingBlock(payloadAttributes engine.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (*types.Block, *engine.ExecutionPayloadEnvelope, error) {
+ parentHash := api.eth.BlockChain().CurrentBlock().Hash()
// If transactions is empty but not nil, build an empty block
// If the transactions is nil, build a block with the current transactions from the txpool
// If the transactions is not nil and not empty, build a block with the transactions
@@ -60,7 +86,7 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
var err error
txs, err = engine.DecodeTransactions(dec)
if err != nil {
- return nil, err
+ return nil, nil, err
}
}
extra := make([]byte, 0)
diff --git a/eth/catalyst/api_testing_test.go b/eth/catalyst/api_testing_test.go
index fd4d28d26a..8b20df13e4 100644
--- a/eth/catalyst/api_testing_test.go
+++ b/eth/catalyst/api_testing_test.go
@@ -119,3 +119,90 @@ func TestBuildBlockV1(t *testing.T) {
}
})
}
+
+func TestCommitBlockV1(t *testing.T) {
+ genesis, blocks := generateMergeChain(5, true)
+ n, ethservice := startEthService(t, genesis, blocks)
+ defer n.Close()
+
+ api := &testingAPI{eth: ethservice}
+ ctx := context.Background()
+
+ nextAttrs := func() engine.PayloadAttributes {
+ head := ethservice.BlockChain().CurrentBlock()
+ return engine.PayloadAttributes{
+ Timestamp: head.Time + 1,
+ Random: crypto.Keccak256Hash([]byte("commit-test")),
+ SuggestedFeeRecipient: head.Coinbase,
+ }
+ }
+
+ t.Run("commitEmptyBlock", func(t *testing.T) {
+ parent := ethservice.BlockChain().CurrentBlock()
+ emptyTxs := []hexutil.Bytes{}
+ hash, err := api.CommitBlockV1(ctx, nextAttrs(), &emptyTxs, nil)
+ if err != nil {
+ t.Fatalf("CommitBlockV1 failed: %v", err)
+ }
+ head := ethservice.BlockChain().CurrentBlock()
+ if head.Hash() != hash {
+ t.Errorf("head hash mismatch: got %x want %x", head.Hash(), hash)
+ }
+ if head.Number.Uint64() != parent.Number.Uint64()+1 {
+ t.Errorf("head number mismatch: got %d want %d", head.Number.Uint64(), parent.Number.Uint64()+1)
+ }
+ block := ethservice.BlockChain().GetBlockByHash(hash)
+ if block == nil {
+ t.Fatal("committed block not found in chain")
+ }
+ if len(block.Transactions()) != 0 {
+ t.Errorf("expected empty block, got %d transactions", len(block.Transactions()))
+ }
+ })
+
+ t.Run("commitBlockWithTransactions", func(t *testing.T) {
+ parent := ethservice.BlockChain().CurrentBlock()
+ nonce, _ := ethservice.APIBackend.GetPoolNonce(ctx, testAddr)
+ tx, _ := types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+ enc, _ := tx.MarshalBinary()
+ txs := []hexutil.Bytes{enc}
+
+ hash, err := api.CommitBlockV1(ctx, nextAttrs(), &txs, nil)
+ if err != nil {
+ t.Fatalf("CommitBlockV1 failed: %v", err)
+ }
+ head := ethservice.BlockChain().CurrentBlock()
+ if head.Hash() != hash {
+ t.Errorf("head hash mismatch: got %x want %x", head.Hash(), hash)
+ }
+ if head.Number.Uint64() != parent.Number.Uint64()+1 {
+ t.Errorf("head number mismatch: got %d want %d", head.Number.Uint64(), parent.Number.Uint64()+1)
+ }
+ block := ethservice.BlockChain().GetBlockByHash(hash)
+ if block == nil {
+ t.Fatal("committed block not found in chain")
+ }
+ if len(block.Transactions()) != 1 {
+ t.Fatalf("expected 1 transaction, got %d", len(block.Transactions()))
+ }
+ if block.Transactions()[0].Hash() != tx.Hash() {
+ t.Errorf("transaction hash mismatch: got %x want %x", block.Transactions()[0].Hash(), tx.Hash())
+ }
+ })
+
+ t.Run("commitWithExtraData", func(t *testing.T) {
+ extra := hexutil.Bytes([]byte("hello"))
+ emptyTxs := []hexutil.Bytes{}
+ hash, err := api.CommitBlockV1(ctx, nextAttrs(), &emptyTxs, &extra)
+ if err != nil {
+ t.Fatalf("CommitBlockV1 failed: %v", err)
+ }
+ block := ethservice.BlockChain().GetBlockByHash(hash)
+ if block == nil {
+ t.Fatal("committed block not found in chain")
+ }
+ if string(block.Extra()) != "hello" {
+ t.Errorf("extraData mismatch: got %q want %q", block.Extra(), "hello")
+ }
+ })
+}
diff --git a/eth/downloader/beaconsync.go b/eth/downloader/beaconsync.go
index 914e1dfada..23daafa8f6 100644
--- a/eth/downloader/beaconsync.go
+++ b/eth/downloader/beaconsync.go
@@ -297,9 +297,11 @@ func (d *Downloader) fetchHeaders(from uint64) error {
return err
}
// If the pivot became stale (older than 2*64-8 (bit of wiggle room)),
- // move it ahead to HEAD-64
+ // move it ahead to HEAD-64.
+ //
+ // The state syncer is consulted first before the pivot movement.
d.pivotLock.Lock()
- if d.pivotHeader != nil {
+ if d.pivotHeader != nil && d.snapSyncer.FrozenPivot() == nil {
if head.Number.Uint64() > d.pivotHeader.Number.Uint64()+2*uint64(fsMinFullBlocks)-8 {
// Retrieve the next pivot header, either from skeleton chain
// or the filled chain
diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go
index e0a4ec6b6d..f23d9396ee 100644
--- a/eth/downloader/downloader.go
+++ b/eth/downloader/downloader.go
@@ -295,12 +295,19 @@ func (d *Downloader) Progress() ethereum.SyncProgress {
SyncedBytecodeBytes: uint64(progress.BytecodeBytes),
SyncedStorage: progress.StorageSynced,
SyncedStorageBytes: uint64(progress.StorageBytes),
+
+ // Snap/1 progress fields
HealedTrienodes: progress.TrienodeHealSynced,
HealedTrienodeBytes: uint64(progress.TrienodeHealBytes),
HealedBytecodes: progress.BytecodeHealSynced,
HealedBytecodeBytes: uint64(progress.BytecodeHealBytes),
HealingTrienodes: progress.HealingTrienodes,
HealingBytecode: progress.HealingBytecode,
+
+ // Snap/2 progress fields
+ SyncedAccessLists: progress.AccessListSynced,
+ TotalAccessLists: progress.AccessListTotal,
+ TrieGenProgress: progress.TrieGenPercent,
}
}
@@ -496,6 +503,18 @@ func (d *Downloader) syncToHead() (err error) {
if mode == ethconfig.SnapSync && pivot == nil {
pivot = d.blockchain.CurrentBlock()
}
+ // If the snap syncer froze its pivot in a previous cycle, resume against
+ // the frozen header instead of a fresh one.
+ if mode == ethconfig.SnapSync && pivot != nil {
+ if frozen := d.snapSyncer.FrozenPivot(); frozen != nil {
+ if rawdb.ReadCanonicalHash(d.stateDB, frozen.Number.Uint64()) == frozen.Hash() {
+ log.Info("Resuming snap sync against frozen pivot", "number", frozen.Number, "hash", frozen.Hash())
+ pivot = frozen
+ } else {
+ log.Warn("Frozen pivot is no longer canonical", "number", frozen.Number, "hash", frozen.Hash())
+ }
+ }
+ }
height := latest.Number.Uint64()
// In beacon mode, use the skeleton chain for the ancestor lookup
@@ -921,7 +940,9 @@ func (d *Downloader) processSnapSyncContent() error {
// the results in the meantime.
//
// Note, there's no issue with memory piling up since after 64 blocks the
- // pivot will forcefully move so these accumulators will be dropped.
+ // pivot will forcefully move so these accumulators will be dropped. The
+ // exception is snap/2 trie generation, where the pivot is frozen on
+ // purpose and results accumulate until the generation finishes.
var (
oldPivot *fetchResult // Locked in pivot block, might change eventually
oldTail []*fetchResult // Downloaded content after the pivot
@@ -978,11 +999,15 @@ func (d *Downloader) processSnapSyncContent() error {
return err
}
if P != nil {
- // If new pivot block found, cancel old state retrieval and restart
+ // If new pivot block found, cancel old state retrieval and restart.
if oldPivot != P {
- sync.Cancel()
- sync = d.syncState(P.Header)
- go closeOnErr(sync)
+ // Skip the restart if the running sync already targets the
+ // pivot's root (e.g, no pivot block movement yet).
+ if sync.pivot.Root != P.Header.Root {
+ sync.Cancel()
+ sync = d.syncState(P.Header)
+ go closeOnErr(sync)
+ }
oldPivot = P
}
// Wait for completion, occasionally checking for pivot staleness
@@ -1114,11 +1139,23 @@ func (d *Downloader) DeliverSnapPacket(peer *snap.Peer, packet snap.Packet) erro
// snap/2 syncer skips snap/1-only peers, which cannot answer its BAL requests.
func (d *Downloader) RegisterSnapPeer(p *snap.Peer) error {
if p.Version() < d.snapSyncer.Version() {
+ // The peer speaks an older snap version than the active syncer needs
+ // (e.g. snap/1 while we sync via snap/2). We still serve it, but it
+ // cannot answer our requests, so it is not registered for syncing.
+ // Surface it so an operator can tell a stalled sync from a quiet one.
+ snapPeerSkipMeter.Mark(1)
+ log.Debug("Skipping snap peer below syncer version", "peer", p.ID(), "version", p.Version(), "required", d.snapSyncer.Version())
return nil
}
return d.snapSyncer.Register(p)
}
+// SnapSyncVersion returns the snap protocol version of the active state syncer.
+// Peers negotiating a lower version cannot serve its requests.
+func (d *Downloader) SnapSyncVersion() uint {
+ return d.snapSyncer.Version()
+}
+
// UnregisterSnapPeer removes a snap peer from the active state syncer. It mirrors
// RegisterSnapPeer's version gate: a peer below the active syncer's version was
// never registered, so there is nothing to remove.
diff --git a/eth/downloader/metrics.go b/eth/downloader/metrics.go
index bfe80ddbf1..caa8dfb9ee 100644
--- a/eth/downloader/metrics.go
+++ b/eth/downloader/metrics.go
@@ -38,4 +38,8 @@ var (
receiptTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/timeout", nil)
throttleCounter = metrics.NewRegisteredCounter("eth/downloader/throttle", nil)
+
+ // snapPeerSkipMeter tracks snap peers skipped by the state syncer because
+ // they negotiated a version below the one the syncer requires.
+ snapPeerSkipMeter = metrics.NewRegisteredMeter("eth/downloader/snap/peerskip", nil)
)
diff --git a/eth/handler.go b/eth/handler.go
index ac5339d092..40a094c53e 100644
--- a/eth/handler.go
+++ b/eth/handler.go
@@ -294,11 +294,17 @@ func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error {
}
reject := false // reserved peer slots
if h.downloader.ConfigSyncMode() == ethconfig.SnapSync {
- if snap == nil {
- // If we are running snap-sync, we want to reserve roughly half the peer
- // slots for peers supporting the snap protocol.
- // The logic here is; we only allow up to 5 more non-snap peers than snap-peers.
- if all, snp := h.peers.len(), h.peers.snapLen(); all-snp > snp+5 {
+ // A peer is useful to the active state syncer only if it offers the snap
+ // extension at (or above) the syncer's version. Non-snap peers AND peers
+ // stuck on an older snap version (e.g. snap/1 while we sync via snap/2)
+ // are both "non-usable": the node still serves them, but they must not be
+ // allowed to fill the slots reserved for peers that can serve the sync.
+ minVersion := h.downloader.SnapSyncVersion()
+ usable := snap != nil && snap.Version() >= minVersion
+ if !usable {
+ // Reserve roughly half the slots for usable peers: only allow up to 5
+ // more non-usable peers (non-snap or below-version snap) than usable ones.
+ if all, snp := h.peers.len(), h.peers.snapLen(minVersion); all-snp > snp+5 {
reject = true
}
}
diff --git a/eth/peerset.go b/eth/peerset.go
index e6f623f90c..fa946e8b17 100644
--- a/eth/peerset.go
+++ b/eth/peerset.go
@@ -49,8 +49,7 @@ var (
// peerSet represents the collection of active peers currently participating in
// the `eth` protocol, with or without the `snap` extension.
type peerSet struct {
- peers map[string]*ethPeer // Peers connected on the `eth` protocol
- snapPeers int // Number of `snap` compatible peers for connection prioritization
+ peers map[string]*ethPeer // Peers connected on the `eth` protocol
snapWait map[string]chan *snap.Peer // Peers connected on `eth` waiting for their snap extension
snapPend map[string]*snap.Peer // Peers connected on the `snap` protocol, but not yet on `eth`
@@ -161,7 +160,6 @@ func (ps *peerSet) registerPeer(peer *eth.Peer, ext *snap.Peer) error {
}
if ext != nil {
eth.snapExt = &snapPeer{ext}
- ps.snapPeers++
}
ps.peers[id] = eth
return nil
@@ -173,14 +171,10 @@ func (ps *peerSet) unregisterPeer(id string) error {
ps.lock.Lock()
defer ps.lock.Unlock()
- peer, ok := ps.peers[id]
- if !ok {
+ if _, ok := ps.peers[id]; !ok {
return errPeerNotRegistered
}
delete(ps.peers, id)
- if peer.snapExt != nil {
- ps.snapPeers--
- }
return nil
}
@@ -210,12 +204,21 @@ func (ps *peerSet) len() int {
return len(ps.peers)
}
-// snapLen returns if the current number of `snap` peers in the set.
-func (ps *peerSet) snapLen() int {
+// snapLen returns the number of `snap` peers whose negotiated version is at
+// least minVersion — i.e. peers usable by a state syncer running that version.
+// Lower-version snap peers (which the node still serves but cannot sync from)
+// are excluded, so they don't get counted toward the reserved snap-peer slots.
+func (ps *peerSet) snapLen(minVersion uint) int {
ps.lock.RLock()
defer ps.lock.RUnlock()
- return ps.snapPeers
+ var n int
+ for _, p := range ps.peers {
+ if p.snapExt != nil && p.snapExt.Version() >= minVersion {
+ n++
+ }
+ }
+ return n
}
// close disconnects all peers.
diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go
index 95c0ed248b..76f3a828a6 100644
--- a/eth/protocols/eth/handler_test.go
+++ b/eth/protocols/eth/handler_test.go
@@ -861,7 +861,7 @@ func testGetPooledTransaction(t *testing.T, blobTx bool) {
emptyBlob = kzg4844.Blob{}
emptyBlobs = []kzg4844.Blob{emptyBlob}
emptyBlobCommit, _ = kzg4844.BlobToCommitment(&emptyBlob)
- emptyBlobProof, _ = kzg4844.ComputeBlobProof(&emptyBlob, emptyBlobCommit)
+ emptyCellProof, _ = kzg4844.ComputeCellProofs(&emptyBlob)
emptyBlobHash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit)
)
backend := newTestBackendWithGenerator(0, true, true, nil)
@@ -885,7 +885,7 @@ func testGetPooledTransaction(t *testing.T, blobTx bool) {
To: testAddr,
BlobHashes: []common.Hash{emptyBlobHash},
BlobFeeCap: uint256.MustFromBig(common.Big1),
- Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion0, emptyBlobs, []kzg4844.Commitment{emptyBlobCommit}, []kzg4844.Proof{emptyBlobProof}),
+ Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion1, emptyBlobs, []kzg4844.Commitment{emptyBlobCommit}, emptyCellProof),
})
if err != nil {
t.Fatal(err)
diff --git a/eth/protocols/snap/bal_apply.go b/eth/protocols/snap/bal_apply.go
index 3e565662e7..d67015361b 100644
--- a/eth/protocols/snap/bal_apply.go
+++ b/eth/protocols/snap/bal_apply.go
@@ -93,7 +93,7 @@ func (s *syncerV2) isStorageFetched(accountHash, storageHash common.Hash) bool {
// applyAccessList applies a single block's access list diffs to the flat state
// in the database. For each account, it applies the post-block values (highest
// TxIdx entry) for balance, nonce, code, and storage. The storageRoot field is
-// intentionally left stale. It will be recomputed during the trie rebuild.
+// intentionally left stale. It will be recomputed during the trie generation.
func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) error {
// Iterate over all accounts in the access list
for _, access := range *b {
@@ -113,7 +113,7 @@ func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) er
rawdb.DeleteStorageSnapshot(batch, accountHash, storageHash)
} else {
// Store the slot in the same encoding the snapshot and the
- // trie rebuild use: RLP of the minimal big-endian value
+ // trie generation use: RLP of the minimal big-endian value
// (leading zeros trimmed), matching core/state's snapshot
// writes.
blob, _ := rlp.EncodeToBytes(value.Bytes())
@@ -176,7 +176,7 @@ func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) er
case isEmpty && !isNew:
// Existing account got fully drained (e.g., pre-funded
// address that gets deployed to with init code that
- // self-destructs). Delete the entry so the trie rebuild
+ // self-destructs). Delete the entry so the trie generation
// doesn't pick it up as an empty leaf.
rawdb.DeleteAccountSnapshot(batch, accountHash)
default:
diff --git a/eth/protocols/snap/bal_apply_test.go b/eth/protocols/snap/bal_apply_test.go
index a9e7f789a5..0db26331ad 100644
--- a/eth/protocols/snap/bal_apply_test.go
+++ b/eth/protocols/snap/bal_apply_test.go
@@ -157,7 +157,7 @@ func TestAccessListApplication(t *testing.T) {
// Verify storage updated. Slots are stored in the canonical snapshot
// encoding (RLP of the value with leading zeros trimmed), the same form
- // the download path writes and the trie rebuild consumes.
+ // the download path writes and the trie generation consumes.
storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
wantStorage, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0x02").Bytes()))
if !bytes.Equal(storageVal, wantStorage) {
@@ -325,10 +325,9 @@ func TestAccessListApplicationSkipsUnfetched(t *testing.T) {
// is below Next so isFetched returns true; the unfetched hash equals Next
// so isFetched returns false.
syncer.tasks = []*accountTaskV2{{
- Next: unfetchedHash,
- Last: common.MaxHash,
- SubTasks: make(map[common.Hash][]*storageTaskV2),
- stateCompleted: make(map[common.Hash]struct{}),
+ Next: unfetchedHash,
+ Last: common.MaxHash,
+ SubTasks: make(map[common.Hash][]*storageTaskV2),
}}
cb := bal.NewConstructionBlockAccessList()
@@ -366,10 +365,9 @@ func TestAccessListApplicationSkipsUnfetchedStorage(t *testing.T) {
}
syncer.tasks = []*accountTaskV2{{
- Next: unfetchedHash,
- Last: common.MaxHash,
- SubTasks: make(map[common.Hash][]*storageTaskV2),
- stateCompleted: make(map[common.Hash]struct{}),
+ Next: unfetchedHash,
+ Last: common.MaxHash,
+ SubTasks: make(map[common.Hash][]*storageTaskV2),
}}
// BAL touches an unfetched account with a storage write AND an empty
@@ -425,7 +423,6 @@ func TestAccessListApplicationPartialStorage(t *testing.T) {
Last: common.MaxHash,
}},
},
- stateCompleted: make(map[common.Hash]struct{}),
}}
cb := bal.NewConstructionBlockAccessList()
diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go
index 7adff5dc0f..ff5564ca65 100644
--- a/eth/protocols/snap/handler.go
+++ b/eth/protocols/snap/handler.go
@@ -84,8 +84,10 @@ type Backend interface {
// otherwise only the default (snap/1) versions are offered on the wire.
func MakeProtocols(backend Backend, snapV2 bool) []p2p.Protocol {
versions := ProtocolVersions
- if snapV2 {
- versions = append([]uint{SNAP2}, versions...)
+ if !snapV2 {
+ // snap/2 is not safe to advertise unconditionally yet, so it is gated
+ // behind a feature flag.
+ versions = []uint{SNAP1}
}
protocols := make([]p2p.Protocol, len(versions))
for i, version := range versions {
diff --git a/eth/protocols/snap/progress_test.go b/eth/protocols/snap/progress_test.go
index 21ec36c2b5..25b304038a 100644
--- a/eth/protocols/snap/progress_test.go
+++ b/eth/protocols/snap/progress_test.go
@@ -186,8 +186,8 @@ func TestSyncProgressV1Discarded(t *testing.T) {
syncer := newSyncerV2(db, rawdb.HashScheme)
syncer.loadSyncStatus()
- if syncer.previousPivot != nil {
- t.Fatalf("expected previousPivot nil after discarding old format, got %+v", syncer.previousPivot)
+ if syncer.pivot != nil {
+ t.Fatalf("expected pivot nil after discarding old format, got %+v", syncer.pivot)
}
if len(syncer.tasks) != accountConcurrency {
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks))
@@ -258,8 +258,8 @@ func TestSyncProgressCorruptPayload(t *testing.T) {
syncer := newSyncerV2(db, rawdb.HashScheme)
syncer.loadSyncStatus()
- if syncer.previousPivot != nil {
- t.Fatalf("expected previousPivot nil after corrupt payload, got %+v", syncer.previousPivot)
+ if syncer.pivot != nil {
+ t.Fatalf("expected pivot nil after corrupt payload, got %+v", syncer.pivot)
}
if len(syncer.tasks) != accountConcurrency {
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks))
diff --git a/eth/protocols/snap/protocol.go b/eth/protocols/snap/protocol.go
index e14ca1283d..d46607ff22 100644
--- a/eth/protocols/snap/protocol.go
+++ b/eth/protocols/snap/protocol.go
@@ -35,11 +35,10 @@ const (
// devp2p capability negotiation.
const ProtocolName = "snap"
-// ProtocolVersions are the supported versions of the `snap` protocol advertised
-// by default (first is primary). snap/2 is not safe to advertise unconditionally
-// yet, so it is gated behind a feature flag and appended in MakeProtocols rather
-// than listed here.
-var ProtocolVersions = []uint{SNAP1}
+// ProtocolVersions are all the `snap` protocol versions this node implements
+// (first is primary). What's actually advertised on the wire is decided by
+// MakeProtocols, which gates snap/2 behind a feature flag.
+var ProtocolVersions = []uint{SNAP2, SNAP1}
// protocolLengths are the number of implemented messages corresponding to
// different protocol versions. snap/2 adds GetAccessLists/AccessLists (0x08/0x09).
diff --git a/eth/protocols/snap/syncer.go b/eth/protocols/snap/syncer.go
index 0c3ea5caf2..b5c1caedf6 100644
--- a/eth/protocols/snap/syncer.go
+++ b/eth/protocols/snap/syncer.go
@@ -41,6 +41,11 @@ type Progress struct {
BytecodeHealBytes common.StorageSize
HealingTrienodes uint64
HealingBytecode uint64
+
+ // snap/2-specific status. Reported by snap/2 only.
+ AccessListSynced uint64 // Block access lists fetched during catch-up
+ AccessListTotal uint64 // Total block access lists to fetch for catch-up
+ TrieGenPercent uint64 // Trie generation completion, in percent (0..100)
}
// Syncer is the uniform view over the snap/1 (*syncer) and snap/2 (*syncerV2)
@@ -58,6 +63,10 @@ type Syncer interface {
OnTrieNodes(peer SyncPeerV2, id uint64, trienodes [][]byte) error
OnAccessLists(peer SyncPeerV2, id uint64, lists rlp.RawList[rlp.RawValue]) error
+ // FrozenPivot returns the pivot header the syncer is bound to, or nil if
+ // the pivot may still be chosen and moved freely.
+ FrozenPivot() *types.Header
+
// Version is the snap protocol version this syncer implements.
Version() uint
}
@@ -122,6 +131,11 @@ func (syncerV1Adapter) OnAccessLists(SyncPeerV2, uint64, rlp.RawList[rlp.RawValu
// Version is SNAP1
func (syncerV1Adapter) Version() uint { return SNAP1 }
+// FrozenPivot is always nil for snap/1: the sync target must keep tracking
+// the chain head, ensuring the state is available in the network, so the
+// pivot is never frozen.
+func (syncerV1Adapter) FrozenPivot() *types.Header { return nil }
+
// syncerV2Adapter adapts the snap/2 *syncerV2 to Syncer. Its peer-facing methods
// already take SyncPeerV2 and its Sync already takes a header, so only Progress
// (different return type) and OnTrieNodes (absent) need wrapping.
@@ -130,12 +144,15 @@ type syncerV2Adapter struct{ *syncerV2 }
func (s syncerV2Adapter) Progress() Progress {
progress := s.syncerV2.Progress()
return Progress{
- AccountSynced: progress.AccountSynced,
- AccountBytes: progress.AccountBytes,
- BytecodeSynced: progress.BytecodeSynced,
- BytecodeBytes: progress.BytecodeBytes,
- StorageSynced: progress.StorageSynced,
- StorageBytes: progress.StorageBytes,
+ AccountSynced: progress.AccountSynced,
+ AccountBytes: progress.AccountBytes,
+ BytecodeSynced: progress.BytecodeSynced,
+ BytecodeBytes: progress.BytecodeBytes,
+ StorageSynced: progress.StorageSynced,
+ StorageBytes: progress.StorageBytes,
+ AccessListSynced: progress.AccessListSynced,
+ AccessListTotal: progress.AccessListTotal,
+ TrieGenPercent: progress.TrieGenPercent,
}
}
diff --git a/eth/protocols/snap/syncv2.go b/eth/protocols/snap/syncv2.go
index 70f78e3ec8..37da0b12b6 100644
--- a/eth/protocols/snap/syncv2.go
+++ b/eth/protocols/snap/syncv2.go
@@ -26,6 +26,7 @@ import (
"math/rand"
"sort"
"sync"
+ "sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
@@ -37,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/msgrate"
+ "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
@@ -54,6 +56,22 @@ const (
// the assumption that the gas limit is 60M.
maxAccessListRequestCount = 28
+ // maxCatchUpBlocks is the maximum gap (in blocks) that BAL catch-up is
+ // allowed to span. BALs are only retained by peers for a limited window
+ // (roughly two weeks, ~100k blocks at 12s block time). If the pivot has
+ // moved further than this conservative bound, the BALs needed to roll the
+ // flat state forward are likely no longer available, so we discard the
+ // stale progress and restart the sync from scratch rather than attempting
+ // a catch-up that is bound to fail partway through.
+ maxCatchUpBlocks = params.FullImmutabilityThreshold
+
+ // catchUpWindow is the number of blocks BAL catch-up fetches and applies at
+ // a time. The whole gap can span up to maxCatchUpBlocks, so fetching it in
+ // one shot would buffer every block's BAL (~100 KiB each) in memory before
+ // applying any. Processing the gap in bounded windows caps peak memory to a
+ // single window's worth of BALs.
+ catchUpWindow = 512
+
// syncProgressVersion is the version byte prepended to the JSON-encoded
// syncProgressV2 when persisted. On load, a mismatching version byte causes
// the persisted progress to be discarded and sync to start fresh.
@@ -68,6 +86,10 @@ const (
// are still hashes left to fetch.
var errAccessListPeersExhausted = errors.New("all peers exhausted for BAL requests")
+// errAccessListUnavailable is returned from the BAL catch-up when some gap
+// block's access list cannot be retrieved against the current peerset.
+var errAccessListUnavailable = errors.New("block access lists unavailable")
+
// accountRequestV2 tracks a pending account range request to ensure responses are
// to actual requests and to validate any security constraints.
//
@@ -261,13 +283,34 @@ type storageTaskV2 struct {
done bool // Flag whether the task can be removed
}
+// syncPhase tracks how far a snap/2 sync has progressed for the journaled
+// pivot. The phases are strictly ordered: each one implies all previous
+// ones have finished.
+type syncPhase uint8
+
+const (
+ // phaseDownload covers the flat state (account, storage, bytecode)
+ // download. The requests target the pivot root, which remote peers
+ // only serve while it is recent, so the pivot must keep tracking the
+ // chain head (see FrozenPivot).
+ phaseDownload syncPhase = iota
+
+ // phaseGenerate covers the local trie generation after the download
+ // has completed. It targets the exact pivot root it was started with,
+ // so pivot updates are refused from here on.
+ phaseGenerate
+
+ // phaseComplete means the sync ran to completion for the pivot.
+ phaseComplete
+)
+
// syncProgressV2 is a database entry to allow suspending and resuming a snapshot state
// sync. Opposed to full and fast sync, there is no way to restart a suspended
// snap sync without prior knowledge of the suspension point.
type syncProgressV2 struct {
- Pivot *types.Header // Pivot header being synced (for pivot move and reorg detection)
- Tasks []*accountTaskV2 // The suspended account tasks (contract tasks within)
- Complete bool // True once sync ran to completion for Pivot
+ Pivot *types.Header // Pivot header being synced (for pivot move and reorg detection)
+ Tasks []*accountTaskV2 // The suspended account tasks (contract tasks within)
+ Phase syncPhase // Phase is how far the sync has progressed for Pivot
// Status report during syncing phase
AccountSynced uint64 // Number of accounts downloaded
@@ -276,6 +319,10 @@ type syncProgressV2 struct {
BytecodeBytes common.StorageSize // Number of bytecode bytes downloaded
StorageSynced uint64 // Number of storage slots downloaded
StorageBytes common.StorageSize // Number of storage trie bytes persisted to disk
+
+ AccessListSynced uint64 `json:"-"` // Block access lists fetched during catch-up
+ AccessListTotal uint64 `json:"-"` // Total block access lists to fetch for catch-up
+ TrieGenPercent uint64 `json:"-"` // Trie generation completion, in percent (0..100)
}
// SyncPeerV2 abstracts out the methods required for a peer to be synced against
@@ -313,7 +360,7 @@ type SyncPeerV2 interface {
// syncerV2 is an Ethereum account and storage trie syncer based on the snap
// protocol. It downloads all accounts, storage slots, and bytecodes from
// remote peers as flat state, applies BAL diffs on pivot moves,
-// and triggers a final trie rebuild once flat state is consistent.
+// and triggers a final trie generation once flat state is consistent.
//
// Every network request has a variety of failure events:
// - The peer disconnects after task assignment, failing to send the request
@@ -322,14 +369,12 @@ type SyncPeerV2 interface {
// - The peer delivers a stale response after a previous timeout
// - The peer delivers a refusal to serve the requested state
type syncerV2 struct {
- db ethdb.Database // Database to store the trie nodes into (and dedup)
- scheme string // Node scheme used in node database
-
- pivot *types.Header // Current pivot header being synced (lock needed)
- previousPivot *types.Header // Pivot from previous sync run (for pivot move detection)
- complete bool // Whether the persisted progress was a completed sync
- tasks []*accountTaskV2 // Current account task set being synced
- update chan struct{} // Notification channel for possible sync progression
+ db ethdb.Database // Database to store the trie nodes into (and dedup)
+ scheme string // Node scheme used in node database
+ pivot *types.Header // Current pivot header being synced (lock needed)
+ phase atomic.Uint32 // Current syncPhase; atomic so phase transitions are visible across goroutines
+ tasks []*accountTaskV2 // Current account task set being synced
+ update chan struct{} // Notification channel for possible sync progression
peers map[string]SyncPeerV2 // Currently active peers to download from
peerJoin *event.Feed // Event feed to react to peers joining
@@ -358,11 +403,17 @@ type syncerV2 struct {
storageSynced uint64 // Number of storage slots downloaded
storageBytes common.StorageSize // Number of storage trie bytes persisted to disk
+ accessListSynced uint64 // Block access lists fetched so far during catch-up
+ accessListTotal uint64 // Block access lists to fetch for the current catch-up
+ genProgress atomic.Uint64 // The live trie-generation progress
+
extProgress *syncProgressV2 // progress that can be exposed to external caller.
startTime time.Time // Time instance when snapshot sync started
logTime time.Time // Time instance when status was last reported
+ catchUpWindow uint64 // Number of blocks fetched/applied per BAL catch-up window (overridable in tests)
+
pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown
lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, pivot)
}
@@ -370,7 +421,7 @@ type syncerV2 struct {
// newSyncerV2 creates a new snapshot syncer to download the Ethereum state over the
// snap protocol.
func newSyncerV2(db ethdb.Database, scheme string) *syncerV2 {
- return &syncerV2{
+ s := &syncerV2{
db: db,
scheme: scheme,
@@ -391,8 +442,27 @@ func newSyncerV2(db ethdb.Database, scheme string) *syncerV2 {
bytecodeReqs: make(map[uint64]*bytecodeRequestV2),
accessListReqs: make(map[uint64]*accessListRequest),
- extProgress: new(syncProgressV2),
+ extProgress: new(syncProgressV2),
+ catchUpWindow: catchUpWindow,
}
+ if raw := rawdb.ReadSnapshotSyncStatus(db); len(raw) > 0 && raw[0] == syncProgressVersion {
+ var progress syncProgressV2
+ if err := json.Unmarshal(raw[1:], &progress); err == nil {
+ s.pivot = progress.Pivot
+ s.setPhase(progress.Phase)
+ }
+ }
+ return s
+}
+
+// getPhase returns the current sync phase.
+func (s *syncerV2) getPhase() syncPhase {
+ return syncPhase(s.phase.Load())
+}
+
+// setPhase moves the sync to the given phase.
+func (s *syncerV2) setPhase(phase syncPhase) {
+ s.phase.Store(uint32(phase))
}
// Register injects a new data source into the syncer's peerset.
@@ -452,19 +522,17 @@ func (s *syncerV2) Unregister(id string) error {
// Sync starts (or resumes a previous) sync cycle to iterate over a state trie
// with the given pivot header and reconstruct the nodes based on the snapshot
// leaves.
-func (s *syncerV2) Sync(pivot *types.Header, cancel chan struct{}) error {
- if pivot == nil {
+func (s *syncerV2) Sync(target *types.Header, cancel chan struct{}) error {
+ if target == nil {
return errors.New("snap sync: pivot header is nil")
}
s.lock.Lock()
- s.pivot = pivot
- s.previousPivot = nil // loadSyncStatus overwrites when resuming from persisted progress
s.statelessPeers = make(map[string]struct{})
s.lock.Unlock()
if s.startTime.IsZero() {
s.startTime = time.Now()
}
- root := pivot.Root
+ root := target.Root
// Retrieve the previous sync status from DB. If there's no persisted
// status, sync is either fresh or already complete.
@@ -473,20 +541,24 @@ func (s *syncerV2) Sync(pivot *types.Header, cancel chan struct{}) error {
// isPivotChanged is true when we have prior progress against a different
// pivot. That means we need to roll forward via catchUp, or wipe and
// restart if the prior pivot was reorged out.
- isPivotChanged := s.previousPivot != nil && s.previousPivot.Hash() != s.pivot.Hash()
+ s.lock.RLock()
+ prevPivot := s.pivot
+ s.lock.RUnlock()
+ isPivotChanged := prevPivot != nil && prevPivot.Hash() != target.Hash()
// Skip if we've already finished syncing this pivot.
- if !isPivotChanged && s.complete {
+ if !isPivotChanged && s.getPhase() == phaseComplete {
log.Info("Snap sync already complete for this pivot", "root", root)
return nil
}
- // We're committing to running this sync. Clear the complete flag so a
- // mid-run save (on cancel or error) doesn't persist a stale Complete=true
- // status from a prior pivot.
- s.lock.Lock()
- s.complete = false
- s.lock.Unlock()
+ // We're committing to running this sync. Demote a completed phase so a
+ // mid-run save (on cancel or error) doesn't persist a stale complete
+ // status from a prior pivot. The download remains done, only the trie
+ // generation must be redone against the new pivot.
+ if s.getPhase() == phaseComplete {
+ s.setPhase(phaseGenerate)
+ }
defer func() {
// Whether sync completed or not, disregard any future packets
@@ -511,28 +583,53 @@ func (s *syncerV2) Sync(pivot *types.Header, cancel chan struct{}) error {
log.Debug("Starting snapshot sync cycle", "root", root)
- // If we resumed against a different pivot, decide whether the persisted
- // progress is still usable. If yes, roll forward via BAL catch-up. If not,
- // wipe everything and restart fresh.
- if isPivotChanged {
- if isPivotReorged(s.db, s.previousPivot, s.pivot) {
- log.Warn("Persisted progress unusable, restarting snap sync from scratch",
- "number", s.previousPivot.Number, "oldHash", s.previousPivot.Hash())
+ if !isPivotChanged {
+ if prevPivot != nil {
+ // Resumed against the same pivot. An unclean shutdown may have left
+ // flushed snapshot data the journal doesn't cover.
+ if err := s.pruneStaleState(); err != nil {
+ log.Warn("Persisted progress unusable, restarting snap sync from scratch", "err", err)
+ s.resetSyncState()
+ }
+ }
+ } else {
+ // If we resumed against a different pivot, decide whether the persisted
+ // progress is still usable. If yes, roll forward via BAL catch-up. If not,
+ // wipe everything and restart fresh.
+ switch {
+ case isPivotReorged(s.db, prevPivot, target):
+ log.Warn("Restarting snap sync from scratch", "oldnumber", prevPivot.Number, "oldHash", prevPivot.Hash())
s.resetSyncState()
- } else if err := s.catchUp(cancel); err != nil {
- return err
+ case catchUpExceedsRetention(prevPivot, target):
+ // The pivot moved further than the BAL retention window. The access
+ // lists required for catch-up are almost certainly unavailable from
+ // peers, so discard the stale progress and resync from scratch
+ // instead of starting a catch-up doomed to stall.
+ log.Warn("Catch-up gap exceeds BAL retention, restarting snap sync from scratch", "oldnumber", prevPivot.Number, "newnumber", target.Number, "gap", new(big.Int).Sub(target.Number, prevPivot.Number), "limit", maxCatchUpBlocks)
+ s.resetSyncState()
+ default:
+ // An unclean shutdown may have left flushed snapshot data the journal
+ // doesn't cover.
+ if err := s.pruneStaleState(); err != nil {
+ log.Warn("Persisted progress unusable, restarting snap sync from scratch", "err", err)
+ s.resetSyncState()
+ break
+ }
+ // A canonical pivot move past a frozen pivot should be impossible:
+ // the downloader both refuses moves (FrozenPivot) and resumes new
+ // cycles against the frozen header itself. Reaching this branch
+ // frozen indicates a bug on the downloader side; roll the flat
+ // state forward defensively and regenerate.
+ if s.getPhase() >= phaseGenerate {
+ log.Warn("Frozen pivot moved unexpectedly, rolling forward", "frozen", prevPivot.Number, "new", target.Number)
+ }
+ if err := s.catchUp(target, cancel); err != nil {
+ return err
+ }
}
}
-
- // Pin previousPivot to the current pivot before downloadState runs.
- // This is what saveSyncStatus persists. If the download is interrupted
- // and the next Sync gets a different pivot, this is how isPivotReorged
- // recognizes the partial flat state belongs to the old pivot. Without
- // it, isPivotReorged sees nil, skips the reorg branch, and downloadState
- // would resume from the persisted task markers but mix the old pivot's
- // already-downloaded accounts with the new pivot's data.
s.lock.Lock()
- s.previousPivot = s.pivot
+ s.pivot = target
s.lock.Unlock()
log.Info("Starting state download", "root", root)
@@ -541,21 +638,48 @@ func (s *syncerV2) Sync(pivot *types.Header, cancel chan struct{}) error {
}
log.Info("State download complete", "root", root)
+ // Entering the generation phase makes the downloader stop moving the
+ // pivot (see FrozenPivot) until the pivot block is committed. The phase
+ // is persisted right away so the freeze also holds across a restart,
+ // before the generation has had a chance to finish.
+ if s.getPhase() < phaseGenerate {
+ s.setPhase(phaseGenerate)
+ s.saveSyncStatus()
+ }
+
log.Info("Starting trie generation", "root", root)
- if _, err := triedb.GenerateTrie(s.db, s.scheme, root, cancel); err != nil {
+ batch := s.db.NewBatch()
+ s.resetTrienodes(batch)
+ if err := batch.Write(); err != nil {
return err
}
+ _, genErr := triedb.GenerateTrieWithProgress(s.db, s.scheme, root, cancel, &s.genProgress)
+ if genErr != nil {
+ return genErr
+ }
log.Info("Trie generation complete", "root", root)
- // Mark sync complete. The deferred saveSyncStatus persists this with
- // Complete=true so a follow-up Sync call for the same pivot can skip
- // the work entirely.
- s.lock.Lock()
- s.complete = true
- s.lock.Unlock()
+ // Mark sync complete. The deferred saveSyncStatus persists this so a
+ // follow-up Sync call for the same pivot can skip the work entirely.
+ s.setPhase(phaseComplete)
return nil
}
+// FrozenPivot returns the pivot header the sync is bound to, or nil while
+// the pivot may still move freely. The pivot freezes once the state
+// download completes. The remaining work (trie generation) and the pivot
+// commit is purely local and targets the exact pivot root the download
+// finished with, so from that point on the downloader must neither move the
+// pivot nor start a new cycle against a different one.
+func (s *syncerV2) FrozenPivot() *types.Header {
+ if s.getPhase() < phaseGenerate {
+ return nil
+ }
+ s.lock.RLock()
+ defer s.lock.RUnlock()
+ return s.pivot
+}
+
// download runs the bulk flat-state download. It fetches
// account ranges, storage slots, and bytecodes, writing flat state to disk.
func (s *syncerV2) downloadState(cancel chan struct{}) error {
@@ -575,6 +699,7 @@ func (s *syncerV2) downloadState(cancel chan struct{}) error {
accountResps = make(chan *accountResponseV2)
storageResps = make(chan *storageResponseV2)
bytecodeResps = make(chan *bytecodeResponseV2)
+ lastJournal = time.Now()
)
for {
// Remove all completed tasks and terminate if everything's done
@@ -583,7 +708,15 @@ func (s *syncerV2) downloadState(cancel chan struct{}) error {
if len(s.tasks) == 0 {
return nil
}
-
+ // Periodically persist the progress journal. The flat state batches
+ // are flushed continuously, while the journal otherwise only hits
+ // disk on graceful teardown; everything written beyond the journaled
+ // markers is sacrificed on an unclean shutdown, so the save interval
+ // bounds the loss.
+ if time.Since(lastJournal) > time.Minute {
+ lastJournal = time.Now()
+ s.saveSyncStatus()
+ }
// Assign all the data retrieval tasks to any free peers
s.assignAccountTasks(accountResps, accountReqFails, cancel)
s.assignBytecodeTasks(bytecodeResps, bytecodeReqFails, cancel)
@@ -591,15 +724,9 @@ func (s *syncerV2) downloadState(cancel chan struct{}) error {
// Update sync progress
s.lock.Lock()
- s.extProgress = &syncProgressV2{
- AccountSynced: s.accountSynced,
- AccountBytes: s.accountBytes,
- BytecodeSynced: s.bytecodeSynced,
- BytecodeBytes: s.bytecodeBytes,
- StorageSynced: s.storageSynced,
- StorageBytes: s.storageBytes,
- }
+ s.refreshProgressLocked()
s.lock.Unlock()
+
// Wait for something to happen
select {
case <-s.update:
@@ -657,79 +784,102 @@ func isPivotReorged(db ethdb.Database, prev, curr *types.Header) bool {
return canonical != prev.Hash()
}
+// catchUpExceedsRetention reports whether rolling the flat state forward from
+// prev to curr would span more blocks than peers are expected to retain BALs
+// for. Beyond this bound the access lists needed for catch-up are likely gone,
+// so the caller should wipe and resync from scratch instead.
+func catchUpExceedsRetention(prev, curr *types.Header) bool {
+ gap := new(big.Int).Sub(curr.Number, prev.Number)
+ return gap.Cmp(big.NewInt(maxCatchUpBlocks)) > 0
+}
+
// catchUp runs the BAL catch-up. When the pivot has moved, it fetches BALs
// for the gap blocks, verifies them against block headers, and applies the
// diffs to roll flat state forward.
-func (s *syncerV2) catchUp(cancel chan struct{}) error {
+func (s *syncerV2) catchUp(target *types.Header, cancel chan struct{}) error {
s.lock.RLock()
- from := s.previousPivot.Number.Uint64() + 1
- to := s.pivot.Number.Uint64()
+ from := s.pivot.Number.Uint64() + 1
+ to := target.Number.Uint64()
s.lock.RUnlock()
log.Info("Starting BAL catch-up", "from", from, "to", to, "blocks", to-from+1)
- // Collect block hashes and headers for the gap range.
- var (
- hashes = make([]common.Hash, 0, to-from+1)
- headers = make(map[common.Hash]*types.Header, to-from+1)
- )
- for num := from; num <= to; num++ {
- hash := rawdb.ReadCanonicalHash(s.db, num)
- if hash == (common.Hash{}) {
- return fmt.Errorf("missing canonical hash for block %d during catch-up", num)
- }
- header := rawdb.ReadHeader(s.db, hash, num)
- if header == nil {
- return fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, hash)
- }
- hashes = append(hashes, hash)
- headers[hash] = header
- }
+ s.lock.Lock()
+ s.accessListTotal = to - from + 1
+ s.accessListSynced = 0
+ s.refreshProgressLocked()
+ s.lock.Unlock()
- // Fetch BALs from peers
- rawBALs, err := s.fetchAccessLists(hashes, headers, cancel)
- if err != nil {
- return err
- }
-
- // Apply each BAL in block order. BALs are already verified by fetchAccessLists.
- for i, raw := range rawBALs {
+ for start := from; start <= to; start += s.catchUpWindow {
select {
case <-cancel:
return ErrCancelled
default:
}
- num := from + uint64(i)
- hash := hashes[i]
-
- // Decode the raw RLP into a BAL.
+ end := start + s.catchUpWindow - 1
+ if end > to {
+ end = to
+ }
+ // Collect block hashes and headers for this window.
var (
- b bal.BlockAccessList
- batch = s.db.NewBatch()
+ hashes = make([]common.Hash, 0, end-start+1)
+ headers = make(map[common.Hash]*types.Header, end-start+1)
)
- if err := rlp.DecodeBytes(raw, &b); err != nil {
- return fmt.Errorf("failed to decode BAL for block %d: %v", num, err)
+ for num := start; num <= end; num++ {
+ hash := rawdb.ReadCanonicalHash(s.db, num)
+ if hash == (common.Hash{}) {
+ return fmt.Errorf("missing canonical hash for block %d during catch-up", num)
+ }
+ header := rawdb.ReadHeader(s.db, hash, num)
+ if header == nil {
+ return fmt.Errorf("missing header for block %d (hash %v) during catch-up", num, hash)
+ }
+ hashes = append(hashes, hash)
+ headers[hash] = header
}
- // applyAccessList failures are persistent. If a block's apply fails
- // here, the next Sync will resume from this block and hit the same
- // failure. Auto-recovery isn't implemented yet.
- if err := s.applyAccessList(&b, batch); err != nil {
- return fmt.Errorf("BAL application failed for block %d: %v", num, err)
- }
-
- // Persist incremental progress so a crash mid-catchUp can resume
- // from the next unapplied block.
- s.lock.Lock()
- s.previousPivot = headers[hash]
- s.lock.Unlock()
- s.saveSyncStatusWithDB(batch)
-
- // Commit the state transition alongside the sync progress atomically.
- if err := batch.Write(); err != nil {
+ // Fetch this window's BALs from peers.
+ rawBALs, err := s.fetchAccessLists(hashes, headers, cancel)
+ if err != nil {
return err
}
+
+ // Apply each BAL in block order. BALs are already verified by fetchAccessLists.
+ for i, raw := range rawBALs {
+ select {
+ case <-cancel:
+ return ErrCancelled
+ default:
+ }
+ num := start + uint64(i)
+ hash := hashes[i]
+
+ // Decode the raw RLP into a BAL.
+ var (
+ b bal.BlockAccessList
+ batch = s.db.NewBatch()
+ )
+ if err := rlp.DecodeBytes(raw, &b); err != nil {
+ return fmt.Errorf("failed to decode BAL for block %d: %v", num, err)
+ }
+ if err := s.applyAccessList(&b, batch); err != nil {
+ return fmt.Errorf("BAL application failed for block %d: %v", num, err)
+ }
+
+ // Persist incremental progress so a crash mid-catchUp can resume
+ // from the next unapplied block.
+ s.lock.Lock()
+ s.pivot = headers[hash]
+ s.lock.Unlock()
+ s.saveSyncStatusWithDB(batch)
+
+ // Commit the state transition alongside the sync progress atomically.
+ if err := batch.Write(); err != nil {
+ return err
+ }
+ }
+ log.Info("BAL catch-up progress", "applied", end, "target", to, "remaining", to-end)
}
- log.Info("BAL catch-up complete", "blocks", len(rawBALs))
+ log.Info("BAL catch-up complete", "from", from, "to", to)
return nil
}
@@ -756,25 +906,22 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has
}
fetched := make(map[common.Hash]rlp.RawValue, len(hashes))
+ // refused tracks the mapping between BAL and peerset which doesn't have
+ // it available.
+ refused := make(map[common.Hash]map[string]struct{})
+
var (
accessListReqFails = make(chan *accessListRequest)
accessListResps = make(chan *accessListResponse)
lastStallLog = time.Now()
)
for len(fetched) < len(hashes) {
- // Assign BAL retrieval tasks to idle peers
- s.assignAccessListTasks(pending, accessListResps, accessListReqFails, cancel)
-
- // If every peer is now stateless and nothing is in flight, no event
- // short of cancel or a new peer joining can move us forward. Surface
- // this so the caller can return and let a higher-level retry happen
- // against a fresh peer set.
- //
- // TODO(rjl, jonny) add a time allowance before returning the error.
- if s.accessListPeersExhausted() {
- log.Warn("BAL peers exhausted, stopping catch-up early", "fetched", len(fetched), "remaining", len(pending))
- return nil, errAccessListPeersExhausted
+ if err := s.checkAccessListProgress(pending, refused); err != nil {
+ log.Warn("BAL fetch cannot progress", "err", err, "fetched", len(fetched), "remaining", len(pending))
+ return nil, err
}
+ // Assign BAL retrieval tasks to idle peers
+ s.assignAccessListTasks(pending, refused, accessListResps, accessListReqFails, cancel)
// Periodic visibility while stalled with peers connected but idle.
if len(pending) > 0 && time.Since(lastStallLog) > 30*time.Second {
@@ -790,13 +937,23 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has
// A new peer joined, try to assign it work
case id := <-peerDrop:
s.revertBALRequests(id, pending)
+ for h, set := range refused {
+ delete(set, id)
+ if len(set) == 0 {
+ delete(refused, h)
+ }
+ }
case <-cancel:
return nil, ErrCancelled
case req := <-accessListReqFails:
s.revertAccessListRequest(req, pending)
case res := <-accessListResps:
- s.processAccessListResponse(res, headers, pending, fetched)
+ s.processAccessListResponse(res, headers, pending, fetched, refused)
}
+ s.lock.Lock()
+ s.accessListSynced += uint64(len(fetched))
+ s.refreshProgressLocked()
+ s.lock.Unlock()
}
// Assemble results in input order
results := make([]rlp.RawValue, len(hashes))
@@ -807,8 +964,9 @@ func (s *syncerV2) fetchAccessLists(hashes []common.Hash, headers map[common.Has
}
// assignAccessListTasks attempts to assign BAL fetch requests to idle
-// peers for any hashes still in pending.
-func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, success chan *accessListResponse, fail chan *accessListRequest, cancel chan struct{}) {
+// peers for any hashes still in pending. Hashes a peer has already refused
+// (recorded in refused) are not assigned back to that same peer.
+func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, refused map[common.Hash]map[string]struct{}, success chan *accessListResponse, fail chan *accessListRequest, cancel chan struct{}) {
s.lock.Lock()
defer s.lock.Unlock()
@@ -822,6 +980,33 @@ func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, succe
)
idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:]
+ // Collect hashes to fetch, capped by peer capacity and the
+ // EIP-8189 2 MiB response soft limit (~72 KiB/BAL -> 28 blocks).
+ if cap > maxAccessListRequestCount {
+ cap = maxAccessListRequestCount
+ }
+ batch := make([]common.Hash, 0, cap)
+ for h := range pending {
+ // Skip hashes this peer has already refused; another peer
+ // must serve them.
+ if set := refused[h]; set != nil {
+ if _, ok := set[idle]; ok {
+ continue
+ }
+ }
+ delete(pending, h)
+
+ batch = append(batch, h)
+ if len(batch) >= cap {
+ break
+ }
+ }
+ // The peer has already refused every pending hash; leave them in
+ // pending for another peer and move on without a wasted request.
+ if len(batch) == 0 {
+ continue
+ }
+
// Generate a unique request ID
var reqid uint64
for {
@@ -834,21 +1019,6 @@ func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, succe
}
break
}
-
- // Collect hashes to fetch, capped by peer capacity and the
- // EIP-8189 2 MiB response soft limit (~72 KiB/BAL -> 28 blocks).
- if cap > maxAccessListRequestCount {
- cap = maxAccessListRequestCount
- }
- batch := make([]common.Hash, 0, cap)
- for h := range pending {
- delete(pending, h)
-
- batch = append(batch, h)
- if len(batch) >= cap {
- break
- }
- }
req := &accessListRequest{
peer: idle,
id: reqid,
@@ -883,7 +1053,7 @@ func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, succe
// processAccessListResponse handles a successful BAL response. It
// verifies each non-empty BAL against the corresponding block header and
// stores the verified ones in fetched.
-func (s *syncerV2) processAccessListResponse(res *accessListResponse, headers map[common.Hash]*types.Header, pending map[common.Hash]struct{}, fetched map[common.Hash]rlp.RawValue) {
+func (s *syncerV2) processAccessListResponse(res *accessListResponse, headers map[common.Hash]*types.Header, pending map[common.Hash]struct{}, fetched map[common.Hash]rlp.RawValue, refused map[common.Hash]map[string]struct{}) {
var (
stateless bool
valid = make(map[common.Hash]rlp.RawValue)
@@ -892,8 +1062,14 @@ func (s *syncerV2) processAccessListResponse(res *accessListResponse, headers ma
for i, raw := range res.accessLists {
h := res.req.hashes[i]
- // Peer doesn't have this BAL. Add it back to pending for retry.
+ // Peer doesn't have this BAL (a legitimate reply, e.g. the block is
+ // outside its retention window). Record the refusal and add the hash
+ // back to pending for a retry against other peers.
if bytes.Equal(raw, rlp.EmptyString) {
+ if refused[h] == nil {
+ refused[h] = make(map[string]struct{})
+ }
+ refused[h][res.req.peer] = struct{}{}
continue
}
var b bal.BlockAccessList
@@ -917,6 +1093,7 @@ func (s *syncerV2) processAccessListResponse(res *accessListResponse, headers ma
// Re-add hashes that were not served back or invalid to pending
for i := 0; i < len(res.req.hashes); i++ {
if _, ok := valid[res.req.hashes[i]]; ok {
+ delete(refused, res.req.hashes[i])
continue
}
pending[res.req.hashes[i]] = struct{}{}
@@ -952,14 +1129,19 @@ func (s *syncerV2) loadSyncStatus() {
}
task.StorageCompleted = nil
}
- s.previousPivot = progress.Pivot
- s.complete = progress.Complete
+ s.pivot = progress.Pivot
+ s.setPhase(progress.Phase)
s.accountSynced = progress.AccountSynced
s.accountBytes = progress.AccountBytes
s.bytecodeSynced = progress.BytecodeSynced
s.bytecodeBytes = progress.BytecodeBytes
s.storageSynced = progress.StorageSynced
s.storageBytes = progress.StorageBytes
+
+ // Seed the externally-exposed snapshot from the restored counters so
+ // eth_syncing reports real stats during catch-up and trie generation
+ // after a resume, instead of the zero-valued initial snapshot.
+ s.refreshProgressLocked()
return
}
}
@@ -979,13 +1161,20 @@ func increaseKey(key []byte) []byte {
return nil
}
-// DeleteHistoryByRange completely removes all database entries with the specific
-// prefix. Note, this method assumes the space with the given prefix is exclusively
-// occupied!
+// deleteRange completely removes all database entries with the specific
+// prefix. Note, this method assumes the space with the given prefix is
+// exclusively occupied!
func deleteRange(batch ethdb.Batch, prefix []byte) {
- start := prefix
- limit := increaseKey(bytes.Clone(prefix))
+ deleteKeyRange(batch, prefix, increaseKey(bytes.Clone(prefix)))
+}
+// deleteKeyRange removes all database entries within the key range [start,
+// limit). Note, this method assumes the range is exclusively occupied by
+// sync data!
+func deleteKeyRange(batch ethdb.Batch, start, limit []byte) {
+ if limit != nil && bytes.Compare(start, limit) >= 0 {
+ return
+ }
// Try to remove the data in the range by a loop, as the leveldb
// doesn't support the native range deletion.
for {
@@ -997,7 +1186,9 @@ func deleteRange(batch ethdb.Batch, prefix []byte) {
// therefore inconsistent. This is a tradeoff of the current LevelDB-based
// approach.
if errors.Is(err, ethdb.ErrTooManyKeys) {
- batch.Write()
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to flush state deletions", "err", err)
+ }
batch.Reset()
continue
}
@@ -1005,6 +1196,82 @@ func deleteRange(batch ethdb.Batch, prefix []byte) {
}
}
+// resetTrienodes wipes all persisted trienodes if the path scheme is used.
+// It's a defensive operation, ensuring all the leftover trie nodes are cleared
+// before the new generation cycle.
+func (s *syncerV2) resetTrienodes(batch ethdb.Batch) {
+ if s.scheme == rawdb.PathScheme {
+ deleteRange(batch, rawdb.TrieNodeAccountPrefix)
+ deleteRange(batch, rawdb.TrieNodeStoragePrefix)
+ }
+}
+
+// pruneStaleState removes any flat state the persisted journal does not cover.
+func (s *syncerV2) pruneStaleState() error {
+ var (
+ batch = s.db.NewBatch()
+ accountKey = func(h common.Hash) []byte {
+ return append(bytes.Clone(rawdb.SnapshotAccountPrefix), h.Bytes()...)
+ }
+ storageKey = func(hashes ...common.Hash) []byte {
+ key := bytes.Clone(rawdb.SnapshotStoragePrefix)
+ for _, h := range hashes {
+ key = append(key, h.Bytes()...)
+ }
+ return key
+ }
+ )
+ keyRangeLimit := func(base []byte, last common.Hash) []byte {
+ if last != common.MaxHash {
+ return append(base, incHash(last).Bytes()...)
+ }
+ return increaseKey(base)
+ }
+ for _, task := range s.tasks {
+ deleteKeyRange(batch, accountKey(task.Next), keyRangeLimit(bytes.Clone(rawdb.SnapshotAccountPrefix), task.Last))
+
+ protected := make([]common.Hash, 0, len(task.stateCompleted)+len(task.SubTasks))
+ for hash := range task.stateCompleted {
+ if bytes.Compare(hash[:], task.Next[:]) < 0 {
+ return errors.New("unexpected storage marker before the range")
+ }
+ if bytes.Compare(hash[:], task.Last[:]) > 0 {
+ return errors.New("unexpected storage marker after the range")
+ }
+ protected = append(protected, hash)
+ }
+ for hash := range task.SubTasks {
+ if _, ok := task.stateCompleted[hash]; ok {
+ return errors.New("unexpected duplicated storage marker")
+ }
+ if bytes.Compare(hash[:], task.Next[:]) < 0 {
+ return errors.New("unexpected storage marker before the range")
+ }
+ if bytes.Compare(hash[:], task.Last[:]) > 0 {
+ return errors.New("unexpected storage marker after the range")
+ }
+ protected = append(protected, hash)
+ }
+ sort.Slice(protected, func(i, j int) bool {
+ return bytes.Compare(protected[i][:], protected[j][:]) < 0
+ })
+
+ start := storageKey(task.Next)
+ for _, hash := range protected {
+ deleteKeyRange(batch, start, storageKey(hash))
+ start = increaseKey(storageKey(hash))
+ }
+ deleteKeyRange(batch, start, keyRangeLimit(bytes.Clone(rawdb.SnapshotStoragePrefix), task.Last))
+
+ for account, subtasks := range task.SubTasks {
+ for _, sub := range subtasks {
+ deleteKeyRange(batch, storageKey(account, sub.Next), keyRangeLimit(storageKey(account), sub.Last))
+ }
+ }
+ }
+ return batch.Write()
+}
+
// resetSyncState wipes all persisted snap-sync data (sync status, account
// and storage snapshots) and re-initializes in-memory state with a fresh
// chunking of the account hash range.
@@ -1013,17 +1280,21 @@ func (s *syncerV2) resetSyncState() {
rawdb.DeleteSnapshotSyncStatus(batch)
deleteRange(batch, rawdb.SnapshotAccountPrefix)
deleteRange(batch, rawdb.SnapshotStoragePrefix)
+ s.resetTrienodes(batch)
batch.Write()
s.lock.Lock()
defer s.lock.Unlock()
s.tasks = nil
- s.previousPivot = nil
- s.complete = false
+ s.pivot = nil
+ s.setPhase(phaseDownload)
s.accountSynced, s.accountBytes = 0, 0
s.bytecodeSynced, s.bytecodeBytes = 0, 0
s.storageSynced, s.storageBytes = 0, 0
+ s.accessListSynced, s.accessListTotal = 0, 0
+ s.genProgress.Store(0)
+ s.refreshProgressLocked()
var next common.Hash
step := new(big.Int).Sub(
@@ -1069,9 +1340,9 @@ func (s *syncerV2) saveSyncStatusWithDB(db ethdb.KeyValueWriter) {
}
// Store the actual progress markers.
progress := &syncProgressV2{
- Pivot: s.previousPivot,
+ Pivot: s.pivot,
Tasks: s.tasks,
- Complete: s.complete,
+ Phase: s.getPhase(),
AccountSynced: s.accountSynced,
AccountBytes: s.accountBytes,
BytecodeSynced: s.bytecodeSynced,
@@ -1088,11 +1359,29 @@ func (s *syncerV2) saveSyncStatusWithDB(db ethdb.KeyValueWriter) {
rawdb.WriteSnapshotSyncStatus(db, status)
}
+// refreshProgressLocked rebuilds the externally-exposed progress snapshot from
+// the live counters. The caller must hold s.lock.
+func (s *syncerV2) refreshProgressLocked() {
+ s.extProgress = &syncProgressV2{
+ AccountSynced: s.accountSynced,
+ AccountBytes: s.accountBytes,
+ BytecodeSynced: s.bytecodeSynced,
+ BytecodeBytes: s.bytecodeBytes,
+ StorageSynced: s.storageSynced,
+ StorageBytes: s.storageBytes,
+ AccessListSynced: s.accessListSynced,
+ AccessListTotal: s.accessListTotal,
+ }
+}
+
// Progress returns the snap sync status statistics.
func (s *syncerV2) Progress() *syncProgressV2 {
s.lock.Lock()
defer s.lock.Unlock()
- return s.extProgress
+
+ p := *s.extProgress
+ p.TrieGenPercent = s.genProgress.Load()
+ return &p
}
// cleanAccountTasks removes account range retrieval tasks that have already been
@@ -2028,7 +2317,7 @@ func (s *syncerV2) forwardAccountTask(task *accountTaskV2) {
// Persist the received account segments. These flat state maybe
// outdated during the sync, but it can be fixed later during the
- // trie rebuild.
+ // trie generation.
oldAccountBytes := s.accountBytes
batch := ethdb.HookedBatch{
@@ -2554,25 +2843,45 @@ func (s *syncerV2) reportSyncProgressV2(force bool) {
"accounts", accounts, "slots", storage, "codes", bytecode, "eta", common.PrettyDuration(estTime-elapsed))
}
-// accessListPeersExhausted reports whether forward progress on BAL fetches is
-// impossible: at least one peer is connected, every connected peer is marked
-// stateless, and no BAL requests are in flight.
-func (s *syncerV2) accessListPeersExhausted() bool {
+// checkAccessListProgress reports whether the BAL fetch can still make
+// forward progress against the current peer set.
+func (s *syncerV2) checkAccessListProgress(pending map[common.Hash]struct{}, refused map[common.Hash]map[string]struct{}) error {
s.lock.RLock()
defer s.lock.RUnlock()
if len(s.peers) == 0 {
- return false
+ return nil
}
if len(s.accessListReqs) > 0 {
- return false
+ return nil
}
+ serviceable := make(map[string]struct{}, len(s.peers))
for id := range s.peers {
if _, ok := s.statelessPeers[id]; !ok {
- return false
+ serviceable[id] = struct{}{}
}
}
- return true
+ if len(serviceable) == 0 {
+ return errAccessListPeersExhausted
+ }
+ for h, set := range refused {
+ // Delivered by some other peer after all
+ if _, ok := pending[h]; !ok {
+ continue
+ }
+ unobtainable := true
+ for id := range serviceable {
+ if _, ok := set[id]; !ok {
+ unobtainable = false
+ break
+ }
+ }
+ if unobtainable {
+ log.Warn("Access list unavailable from all peers", "hash", h)
+ return errAccessListUnavailable
+ }
+ }
+ return nil
}
// sortIdlePeers builds a list of idle peers sorted by download capacity
diff --git a/eth/protocols/snap/syncv2_test.go b/eth/protocols/snap/syncv2_test.go
index d3c1b61800..9e366b108c 100644
--- a/eth/protocols/snap/syncv2_test.go
+++ b/eth/protocols/snap/syncv2_test.go
@@ -547,6 +547,65 @@ func testSyncV2(t *testing.T, scheme string) {
verifyAdoptedSyncedState(scheme, syncer.db, sourceAccountTrie.Hash(), elems, t)
}
+// TestSyncV2FrozenPivot checks the pivot freeze signal around the sync
+// lifecycle. The pivot is unfrozen while flat state is downloading, frozen
+// once the download completes, stays frozen after the sync returns so the
+// downloader resumes against it until the pivot block is committed, and
+// unfreezes again after a state reset.
+func TestSyncV2FrozenPivot(t *testing.T) {
+ t.Parallel()
+ testSyncV2FrozenPivot(t, rawdb.HashScheme)
+ testSyncV2FrozenPivot(t, rawdb.PathScheme)
+}
+
+func testSyncV2FrozenPivot(t *testing.T, scheme string) {
+ var (
+ once sync.Once
+ cancel = make(chan struct{})
+ term = func() { once.Do(func() { close(cancel) }) }
+ )
+ nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
+
+ source := newTestPeerV2("source", t, term)
+ source.accountTrie = sourceAccountTrie.Copy()
+ source.accountValues = elems
+
+ syncer := setupSyncerV2(nodeScheme, source)
+ pivot := mkPivot(0, sourceAccountTrie.Hash())
+
+ // The handler runs while account ranges are still being served, so it
+ // can observe the signal mid download.
+ source.accountRequestV2Handler = func(p *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
+ if syncer.FrozenPivot() != nil {
+ t.Error("pivot frozen during flat state download")
+ }
+ return defaultAccountRequestHandlerV2(p, requestId, root, origin, limit, cap)
+ }
+ if syncer.FrozenPivot() != nil {
+ t.Fatal("pivot frozen before sync started")
+ }
+ if err := syncer.Sync(pivot, cancel); err != nil {
+ t.Fatalf("sync failed: %v", err)
+ }
+ if frozen := syncer.FrozenPivot(); frozen == nil || frozen.Hash() != pivot.Hash() {
+ t.Fatal("pivot not frozen at the synced header after download completed")
+ }
+ // A restart must not lose the freeze: a fresh syncer instance on the same
+ // database derives it from the persisted journal, before any Sync call.
+ restarted := newSyncerV2(syncer.db, nodeScheme)
+ if frozen := restarted.FrozenPivot(); frozen == nil || frozen.Hash() != pivot.Hash() {
+ t.Fatal("pivot freeze lost after restart")
+ }
+ syncer.resetSyncState()
+ if syncer.FrozenPivot() != nil {
+ t.Fatal("pivot still frozen after state reset")
+ }
+ // The reset deletes the journal, so a restarted instance is unfrozen too.
+ if restarted := newSyncerV2(syncer.db, nodeScheme); restarted.FrozenPivot() != nil {
+ t.Fatal("pivot still frozen after restart following a state reset")
+ }
+}
+
// verifyAdoptedSyncedState exercises the snap/2 completion contract end-to-end:
// after a real sync, opening a fresh triedb and calling AdoptSyncedState must
// (a) succeed and (b) leave flat-state reads serving immediately, with no
@@ -1326,9 +1385,9 @@ func TestIsPivotReorged(t *testing.T) {
// canonical header at block 100 has a different hash. Sync is then called with
// a new pivot at the same height.
//
-// If isPivotReorged works, loadSyncStatus restores previousPivot, the check
-// flags it as reorged, resetSyncState clears previousPivot, catchUp is
-// skipped, and the fresh download proceeds to completion.
+// If isPivotReorged works, loadSyncStatus restores the persisted pivot, the
+// check flags it as reorged, resetSyncState clears it, catchUp is skipped,
+// and the fresh download proceeds to completion.
//
// If detection doesn't fire, the pivot-move check would call catchUp with
// from = 101 and to = 100 — the inverted-range guard surfaces that as an
@@ -1347,17 +1406,14 @@ func TestSyncDetectsPivotReorged(t *testing.T) {
// and non-zero counter so the reset path has something to clean up.
orphanPivot := mkPivot(100, common.HexToHash("0xdead"))
seed := newSyncerV2(db, nodeScheme)
- // previousPivot reflects where flat state matches and it is what
- // saveSyncStatus persists. Set it to simulate a prior sync reaching
- // orphanPivot.
- seed.previousPivot = orphanPivot
+ // pivot reflects where flat state matches and it is what saveSyncStatus
+ // persists. Set it to simulate a prior sync reaching orphanPivot.
seed.pivot = orphanPivot
seed.accountSynced = 42
seed.tasks = []*accountTaskV2{{
- Next: common.HexToHash("0x80"),
- Last: common.MaxHash,
- SubTasks: make(map[common.Hash][]*storageTaskV2),
- stateCompleted: make(map[common.Hash]struct{}),
+ Next: common.HexToHash("0x80"),
+ Last: common.MaxHash,
+ SubTasks: make(map[common.Hash][]*storageTaskV2),
}}
seed.saveSyncStatus()
@@ -1391,14 +1447,14 @@ func TestSyncDetectsPivotReorged(t *testing.T) {
if err := syncer.Sync(newPivot, cancel); err != nil {
t.Fatalf("sync failed (reorg detection likely broken): %v", err)
}
- // After successful completion, status should be marked Complete=true
+ // After successful completion, status should reach the complete phase
// against the new (canonical) pivot.
loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus()
- if !loader.complete {
- t.Fatal("sync status should be marked Complete=true after successful completion")
+ if loader.getPhase() != phaseComplete {
+ t.Fatal("sync status should reach the complete phase after successful completion")
}
- if loader.previousPivot == nil || loader.previousPivot.Hash() != newPivot.Hash() {
+ if loader.pivot == nil || loader.pivot.Hash() != newPivot.Hash() {
t.Fatalf("expected persisted pivot to match new pivot")
}
if data := rawdb.ReadAccountSnapshot(db, orphanAccountHash); len(data) != 0 {
@@ -1445,9 +1501,8 @@ func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
syncer1.Register(src1)
src1.remote = syncer1
pivot := mkPivot(0, root)
- syncer1.pivot = pivot
- syncer1.previousPivot = pivot // Sync sets this before downloadState
syncer1.loadSyncStatus()
+ syncer1.pivot = pivot // Sync pins this before downloadState
syncer1.downloadState(cancel1)
// Save progress
@@ -1483,9 +1538,8 @@ func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
syncer2.Register(src2)
src2.remote = syncer2
pivot2 := mkPivot(0, root)
- syncer2.pivot = pivot2
- syncer2.previousPivot = pivot2 // Sync sets this before downloadState
syncer2.loadSyncStatus()
+ syncer2.pivot = pivot2 // Sync pins this before downloadState
if err := syncer2.downloadState(cancel2); err != nil {
t.Fatalf("resumed download failed: %v", err)
}
@@ -1499,10 +1553,10 @@ func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
}
// TestSyncPersistsPivotDuringDownload verifies that after a fresh Sync is
-// interrupted mid-download, the persisted previousPivot equals the current
-// pivot (not nil). Without this, a follow-up Sync at a different pivot
-// would not see that the partial flat state belongs to the old pivot, and
-// would mix old-pivot accounts with new-pivot data.
+// interrupted mid-download, the persisted pivot equals the current pivot
+// (not nil). Without this, a follow-up Sync at a different pivot would not
+// see that the partial flat state belongs to the old pivot, and would mix
+// old-pivot accounts with new-pivot data.
func TestSyncPersistsPivotDuringDownload(t *testing.T) {
t.Parallel()
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
@@ -1532,15 +1586,15 @@ func TestSyncPersistsPivotDuringDownload(t *testing.T) {
// Sync should be interrupted by the cancel after a couple of responses.
_ = syncer.Sync(pivot, cancel)
- // Persisted previousPivot must equal the pivot, so a follow-up Sync at a
- // different pivot can recognize the partial flat state belongs to this one.
+ // Persisted pivot must equal the pivot, so a follow-up Sync at a different
+ // pivot can recognize the partial flat state belongs to this one.
loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus()
- if loader.previousPivot == nil {
- t.Fatal("expected persisted previousPivot to be set after interrupted download, got nil")
+ if loader.pivot == nil {
+ t.Fatal("expected persisted pivot to be set after interrupted download, got nil")
}
- if loader.previousPivot.Hash() != pivot.Hash() {
- t.Errorf("persisted previousPivot mismatch: got %v, want %v", loader.previousPivot.Hash(), pivot.Hash())
+ if loader.pivot.Hash() != pivot.Hash() {
+ t.Errorf("persisted pivot mismatch: got %v, want %v", loader.pivot.Hash(), pivot.Hash())
}
}
@@ -1702,7 +1756,7 @@ func testPivotMovement(t *testing.T, scheme string, pivotMoves int) {
}
// TestCatchUpPersistsIncrementally verifies that catchUp updates and persists
-// previousPivot after each successfully applied BAL. If a later block in the
+// the pivot after each successfully applied BAL. If a later block in the
// gap fails to apply, the persisted state reflects the last successful block,
// so a follow-up Sync can resume from there rather than reapplying everything.
func TestCatchUpPersistsIncrementally(t *testing.T) {
@@ -1776,7 +1830,7 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
blocks[i] = balBlock{header: header, bal: buf.Bytes()}
}
- // First sync: complete sync to A so persisted state has previousPivot=A,
+ // First sync: complete sync to A so persisted state has pivot=A,
// flat state covers all accounts.
{
var (
@@ -1826,22 +1880,157 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
t.Fatal("expected Sync to fail when applyAccessList hits corrupt flat state")
}
- // Persisted previousPivot should now reflect the last successfully applied
+ // Persisted pivot should now reflect the last successfully applied
// block (A+2). Without per-iteration saves, it would still be at A.
loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus()
- if loader.previousPivot == nil {
- t.Fatal("expected persisted previousPivot to be set after partial catchUp")
+ if loader.pivot == nil {
+ t.Fatal("expected persisted pivot to be set after partial catchUp")
}
wantHash := blocks[1].header.Hash()
- if loader.previousPivot.Hash() != wantHash {
- t.Errorf("persisted previousPivot mismatch after partial catchUp: got %v, want %v (block A+2)",
- loader.previousPivot.Hash(), wantHash)
+ if loader.pivot.Hash() != wantHash {
+ t.Errorf("persisted pivot mismatch after partial catchUp: got %v, want %v (block A+2)",
+ loader.pivot.Hash(), wantHash)
+ }
+}
+
+// TestCatchUpWindowed verifies that catch-up correctly rolls the flat state
+// forward when the gap spans several windows. With catchUpWindow shrunk to 2,
+// a 5-block gap is processed as three windows ([A+1,A+2], [A+3,A+4], [A+5]),
+// exercising the window boundaries. Every block's BAL must be fetched and
+// applied, and the final pivot must reach the target.
+func TestCatchUpWindowed(t *testing.T) {
+ t.Parallel()
+ testCatchUpWindowed(t, rawdb.HashScheme)
+ testCatchUpWindowed(t, rawdb.PathScheme)
+}
+
+func testCatchUpWindowed(t *testing.T, scheme string) {
+ nodeScheme, sourceAccountTrie, elems, addrs := makeAccountTrieWithAddresses(100, scheme)
+ rootA := sourceAccountTrie.Hash()
+ numA := uint64(100)
+
+ targetAddr := addrs[0]
+ targetHash := crypto.Keccak256Hash(targetAddr[:])
+
+ db := rawdb.NewMemoryDatabase()
+ emptyHash := common.Hash{}
+ zero := uint64(0)
+
+ // Persist header + canonical hash for the base pivot A so reorg detection
+ // in Sync passes and catchUp (not reset) runs.
+ pivotA := &types.Header{
+ Number: new(big.Int).SetUint64(numA), Root: rootA, Difficulty: common.Big0,
+ BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
+ BlobGasUsed: &zero, ExcessBlobGas: &zero,
+ ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
+ }
+ rawdb.WriteHeader(db, pivotA)
+ rawdb.WriteCanonicalHash(db, pivotA.Hash(), numA)
+
+ // Build a 5-block gap, each block bumping targetAddr's balance. The last
+ // block's balance is the expected final state.
+ const gap = 5
+ var (
+ lastHeader *types.Header
+ lastBalance *uint256.Int
+ balsByHash = make(map[common.Hash]rlp.RawValue, gap)
+ )
+ for i := 0; i < gap; i++ {
+ blockNum := numA + uint64(i) + 1
+ balance := uint256.NewInt(uint64(1000 * (i + 1)))
+
+ cb := bal.NewConstructionBlockAccessList()
+ cb.BalanceChange(0, targetAddr, balance)
+ var buf bytes.Buffer
+ if err := cb.EncodeRLP(&buf); err != nil {
+ t.Fatal(err)
+ }
+ var b bal.BlockAccessList
+ if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
+ t.Fatal(err)
+ }
+ balHash := b.Hash()
+ header := &types.Header{
+ Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0,
+ BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
+ BlobGasUsed: &zero, ExcessBlobGas: &zero,
+ ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
+ BlockAccessListHash: &balHash,
+ }
+ rawdb.WriteHeader(db, header)
+ rawdb.WriteCanonicalHash(db, header.Hash(), blockNum)
+ balsByHash[header.Hash()] = buf.Bytes()
+ lastHeader, lastBalance = header, balance
+ }
+
+ // Seed sync to A: persisted state ends with pivot=A and full flat state.
+ {
+ var (
+ once sync.Once
+ cancel = make(chan struct{})
+ term = func() { once.Do(func() { close(cancel) }) }
+ )
+ syncer := newSyncerV2(db, nodeScheme)
+ src := newTestPeerV2("seed", t, term)
+ src.accountTrie = sourceAccountTrie.Copy()
+ src.accountValues = elems
+ syncer.Register(src)
+ src.remote = syncer
+ if err := syncer.Sync(pivotA, cancel); err != nil {
+ t.Fatalf("seed sync failed: %v", err)
+ }
+ }
+
+ // Run catch-up to A+5 directly with a window of 2, forcing three windows
+ // ([A+1,A+2], [A+3,A+4], [A+5]). Calling catchUp in isolation keeps the
+ // focus on the windowing logic without the surrounding download/trie-gen
+ // phases (which would need a real target state root).
+ var (
+ once sync.Once
+ cancel = make(chan struct{})
+ term = func() { once.Do(func() { close(cancel) }) }
+ )
+ syncer := newSyncerV2(db, nodeScheme)
+ syncer.loadSyncStatus() // restores pivot=A from the seed sync
+ if syncer.pivot == nil || syncer.pivot.Number.Uint64() != numA {
+ t.Fatalf("expected restored pivot at block %d, got %v", numA, syncer.pivot)
+ }
+ syncer.catchUpWindow = 2
+ src := newTestPeerV2("catchup", t, term)
+ src.accountTrie = sourceAccountTrie.Copy()
+ src.accountValues = elems
+ src.accessLists = balsByHash
+ syncer.Register(src)
+ src.remote = syncer
+ if err := syncer.catchUp(lastHeader, cancel); err != nil {
+ t.Fatalf("windowed catch-up failed: %v", err)
+ }
+
+ // The account must reflect the final block's balance, proving every window
+ // (including the trailing partial one) was fetched and applied in order.
+ data := rawdb.ReadAccountSnapshot(db, targetHash)
+ if len(data) == 0 {
+ t.Fatal("target account missing after windowed catch-up")
+ }
+ account, err := types.FullAccount(data)
+ if err != nil {
+ t.Fatalf("failed to decode account: %v", err)
+ }
+ if account.Balance.Cmp(lastBalance) != 0 {
+ t.Errorf("balance after windowed catch-up: got %v, want %v", account.Balance, lastBalance)
+ }
+
+ // The persisted pivot must have advanced all the way to the target.
+ loader := newSyncerV2(db, nodeScheme)
+ loader.loadSyncStatus()
+ if loader.pivot == nil || loader.pivot.Hash() != lastHeader.Hash() {
+ t.Errorf("persisted pivot did not reach target after windowed catch-up")
}
}
// TestSyncStatusMarkedCompleteAfterCompletion verifies that after a full sync
-// completes, the persisted sync status has Complete=true. This lets a
+// completes, the persisted sync status reaches the complete phase. This lets a
// subsequent Sync call distinguish "already done" from "fresh node" and skip.
func TestSyncStatusMarkedCompleteAfterCompletion(t *testing.T) {
t.Parallel()
@@ -1870,13 +2059,13 @@ func testSyncStatusMarkedCompleteAfterCompletion(t *testing.T, scheme string) {
}
// After successful sync, persisted status should be present with
- // Complete=true and the pivot we synced to.
+ // the complete phase and the pivot we synced to.
loader := newSyncerV2(syncer.db, nodeScheme)
loader.loadSyncStatus()
- if !loader.complete {
- t.Fatal("expected persisted status to have Complete=true after successful sync")
+ if loader.getPhase() != phaseComplete {
+ t.Fatal("expected persisted status to reach the complete phase after successful sync")
}
- if loader.previousPivot == nil || loader.previousPivot.Hash() != pivot.Hash() {
+ if loader.pivot == nil || loader.pivot.Hash() != pivot.Hash() {
t.Fatalf("expected persisted pivot to match synced pivot")
}
}
@@ -1906,7 +2095,7 @@ func TestSyncSkipsIfAlreadyComplete(t *testing.T) {
t.Fatalf("first sync failed: %v", err)
}
- // Wipe the flat state. The persisted status (with Complete=true) stays.
+ // Wipe the flat state. The persisted status (in the complete phase) stays.
if err := syncer.db.DeleteRange(rawdb.SnapshotAccountPrefix, []byte{rawdb.SnapshotAccountPrefix[0] + 1}); err != nil {
t.Fatalf("failed to wipe account snapshot: %v", err)
}
@@ -1922,17 +2111,17 @@ func TestSyncSkipsIfAlreadyComplete(t *testing.T) {
}
}
-// TestInterruptedRebuildRecovery verifies that if sync is interrupted after
-// download completes but before trie rebuild finishes, the next Sync() call
-// re-runs the download (which completes immediately) and rebuild.
-func TestInterruptedRebuildRecovery(t *testing.T) {
+// TestInterruptedGenerationRecovery verifies that if sync is interrupted after
+// download completes but before trie generation finishes, the next Sync() call
+// re-runs the download (which completes immediately) and generation.
+func TestInterruptedGenerationRecovery(t *testing.T) {
t.Parallel()
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
root := sourceAccountTrie.Hash()
// First run: complete download, save status, simulate interruption
- // before rebuild by calling downloadState() directly
+ // before generation by calling downloadState() directly
var (
once1 sync.Once
cancel1 = make(chan struct{})
@@ -1946,9 +2135,8 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
syncer1.Register(src1)
src1.remote = syncer1
pivot := mkPivot(0, root)
- syncer1.pivot = pivot
- syncer1.previousPivot = pivot // Sync sets this before downloadState
syncer1.loadSyncStatus()
+ syncer1.pivot = pivot // Sync pins this before downloadState
if err := syncer1.downloadState(cancel1); err != nil {
t.Fatalf("download failed: %v", err)
@@ -1960,11 +2148,11 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
syncer1.cleanAccountTasks()
syncer1.saveSyncStatus()
- // Status should exist (rebuild hasn't run yet)
+ // Status should exist (generation hasn't run yet)
if rawdb.ReadSnapshotSyncStatus(db) == nil {
t.Fatal("sync status should exist after download")
}
- // Second run: full Sync should detect tasks are done, run rebuild
+ // Second run: full Sync should detect tasks are done, run generation
var (
once2 sync.Once
cancel2 = make(chan struct{})
@@ -1980,14 +2168,199 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
if err := syncer2.Sync(mkPivot(0, root), cancel2); err != nil {
t.Fatalf("resumed sync failed: %v", err)
}
- // After rebuild completes, status should be marked Complete=true.
+ // The resumed run re-arms the pivot freeze once its no-op download
+ // completes, the downloader relies on it until the pivot block commits.
+ if syncer2.FrozenPivot() == nil {
+ t.Fatal("pivot not frozen after resumed sync")
+ }
+ // After generation completes, status should reach the complete phase.
loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus()
- if !loader.complete {
- t.Fatal("sync status should be marked Complete=true after rebuild completes")
+ if loader.getPhase() != phaseComplete {
+ t.Fatal("sync status should reach the complete phase after generation completes")
}
}
+// TestPruneStaleState verifies that resuming a sync wipes exactly the flat
+// state the journal does not cover: account and storage entries beyond the
+// task cursors are removed, while everything below the cursors — and the
+// journaled storage of completed-storage accounts and chunked large
+// contracts inside the window — survives.
+func TestPruneStaleState(t *testing.T) {
+ t.Parallel()
+
+ var (
+ db = rawdb.NewMemoryDatabase()
+ syncer = newSyncerV2(db, rawdb.HashScheme)
+
+ fetched = common.Hash{0x20} // below the cursor, journal-covered
+ next = common.Hash{0x40} // task cursor
+ stale = common.Hash{0x50} // beyond the cursor, not covered
+ completed = common.Hash{0x60} // beyond the cursor, storage journaled complete
+ chunked = common.Hash{0x80} // beyond the cursor, large contract, partially journaled
+ staleToo = common.Hash{0xa0} // beyond the cursor, not covered
+
+ subNext = common.Hash{0x50} // slot cursor of the chunked contract
+ slotLo = common.Hash{0x11} // below the slot cursor, journal-covered
+ slotHi = common.Hash{0x77} // beyond the slot cursor, not covered
+
+ val = []byte{0xde, 0xad}
+ )
+ syncer.tasks = []*accountTaskV2{{
+ Next: next,
+ Last: common.MaxHash,
+ SubTasks: map[common.Hash][]*storageTaskV2{
+ chunked: {{Next: subNext, Last: common.MaxHash}},
+ },
+ stateCompleted: map[common.Hash]struct{}{completed: {}},
+ }}
+
+ // Journal-covered state: account below the cursor with a storage slot,
+ // the completed account's storage, the chunked contract's low slots.
+ rawdb.WriteAccountSnapshot(db, fetched, val)
+ rawdb.WriteStorageSnapshot(db, fetched, slotLo, val)
+ rawdb.WriteStorageSnapshot(db, completed, slotLo, val)
+ rawdb.WriteStorageSnapshot(db, chunked, slotLo, val)
+
+ // Uncovered state, flushed after the last journal save: accounts beyond
+ // the cursor (including the completed-storage one, whose account entry
+ // is only legitimate below the cursor), their storage, and the chunked
+ // contract's slots beyond its slot cursor.
+ rawdb.WriteAccountSnapshot(db, stale, val)
+ rawdb.WriteAccountSnapshot(db, completed, val)
+ rawdb.WriteAccountSnapshot(db, staleToo, val)
+ rawdb.WriteStorageSnapshot(db, stale, slotLo, val)
+ rawdb.WriteStorageSnapshot(db, staleToo, slotHi, val)
+ rawdb.WriteStorageSnapshot(db, chunked, slotHi, val)
+
+ // Plant bare keys right at the start of the neighboring keyspaces. The
+ // task ranges end at the maximum hash, so a limit computed by bumping
+ // the full key would roll the carry over into these keyspaces and
+ // swallow such keys.
+ neighborOfAccounts := bytes.Clone(rawdb.SnapshotAccountPrefix)
+ neighborOfAccounts[len(neighborOfAccounts)-1]++
+ db.Put(neighborOfAccounts, val)
+
+ neighborOfStorage := bytes.Clone(rawdb.SnapshotStoragePrefix)
+ neighborOfStorage[len(neighborOfStorage)-1]++
+ db.Put(neighborOfStorage, val)
+
+ if err := syncer.pruneStaleState(); err != nil {
+ t.Fatalf("prune failed: %v", err)
+ }
+ for _, key := range [][]byte{neighborOfAccounts, neighborOfStorage} {
+ if has, _ := db.Has(key); !has {
+ t.Errorf("neighboring keyspace entry %x swallowed by the wipe", key)
+ }
+ }
+
+ for _, tc := range []struct {
+ account common.Hash
+ slot *common.Hash
+ want bool
+ desc string
+ }{
+ {fetched, nil, true, "account below cursor"},
+ {fetched, &slotLo, true, "storage below cursor"},
+ {completed, &slotLo, true, "storage of completed account"},
+ {chunked, &slotLo, true, "chunked contract slot below slot cursor"},
+ {stale, nil, false, "account beyond cursor"},
+ {completed, nil, false, "account entry of completed account"},
+ {staleToo, nil, false, "account beyond cursor (second)"},
+ {stale, &slotLo, false, "storage of unprotected account"},
+ {staleToo, &slotHi, false, "storage of unprotected account (second)"},
+ {chunked, &slotHi, false, "chunked contract slot beyond slot cursor"},
+ } {
+ var have bool
+ if tc.slot == nil {
+ have = len(rawdb.ReadAccountSnapshot(db, tc.account)) > 0
+ } else {
+ have = len(rawdb.ReadStorageSnapshot(db, tc.account, *tc.slot)) > 0
+ }
+ if have != tc.want {
+ t.Errorf("%s: present = %v, want %v", tc.desc, have, tc.want)
+ }
+ }
+}
+
+// TestResumeWipesUncoveredState simulates an unclean shutdown mid-download:
+// flat state flushed beyond the journaled cursor (which the journal therefore
+// considers unfetched) must be wiped when the sync resumes — otherwise it
+// would survive the re-download untouched and corrupt the generated trie.
+func TestResumeWipesUncoveredState(t *testing.T) {
+ t.Parallel()
+ testResumeWipesUncoveredState(t, rawdb.HashScheme)
+ testResumeWipesUncoveredState(t, rawdb.PathScheme)
+}
+
+func testResumeWipesUncoveredState(t *testing.T, scheme string) {
+ nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
+ root := sourceAccountTrie.Hash()
+ db := rawdb.NewMemoryDatabase()
+ pivot := mkPivot(0, root)
+
+ // First run: interrupt the download after a couple of responses, leaving
+ // a journal behind (saved on the graceful teardown).
+ var (
+ once1 sync.Once
+ cancel1 = make(chan struct{})
+ term1 = func() { once1.Do(func() { close(cancel1) }) }
+ responses atomic.Int32
+ )
+ syncer1 := newSyncerV2(db, nodeScheme)
+ src1 := newTestPeerV2("source1", t, term1)
+ src1.accountTrie = sourceAccountTrie.Copy()
+ src1.accountValues = elems
+ src1.accountRequestV2Handler = func(tp *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
+ if responses.Add(1) > 2 {
+ term1()
+ return nil
+ }
+ return defaultAccountRequestHandlerV2(tp, id, root, origin, limit, cap)
+ }
+ syncer1.Register(src1)
+ src1.remote = syncer1
+ syncer1.Sync(pivot, cancel1)
+
+ // Simulate the unclean shutdown: plant a bogus account that the journal
+ // does not cover (right beyond an unfinished task's cursor) — as if it
+ // was flushed after the journal was last saved. The account is not part
+ // of the source trie, so the re-download would never overwrite it and
+ // trie generation would fail the root check if it survived.
+ loader := newSyncerV2(db, nodeScheme)
+ loader.loadSyncStatus()
+ if len(loader.tasks) == 0 {
+ t.Fatal("expected unfinished tasks in the journal")
+ }
+ task := loader.tasks[len(loader.tasks)-1]
+ bogus := incHash(task.Next)
+ rawdb.WriteAccountSnapshot(db, bogus, types.SlimAccountRLP(types.StateAccount{
+ Nonce: 1, Balance: uint256.NewInt(1),
+ Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash[:],
+ }))
+
+ // Second run: the resume must prune the bogus entry and complete cleanly.
+ var (
+ once2 sync.Once
+ cancel2 = make(chan struct{})
+ term2 = func() { once2.Do(func() { close(cancel2) }) }
+ )
+ syncer2 := newSyncerV2(db, nodeScheme)
+ src2 := newTestPeerV2("source2", t, term2)
+ src2.accountTrie = sourceAccountTrie.Copy()
+ src2.accountValues = elems
+ syncer2.Register(src2)
+ src2.remote = syncer2
+
+ if err := syncer2.Sync(pivot, cancel2); err != nil {
+ t.Fatalf("resumed sync failed: %v", err)
+ }
+ if len(rawdb.ReadAccountSnapshot(db, bogus)) != 0 {
+ t.Fatal("uncovered account entry should have been pruned on resume")
+ }
+ verifyTrie(scheme, db, root, t)
+}
+
// TestFetchAccessListsMultiplePeers verifies that fetch distributes work
// across multiple idle peers.
func TestFetchAccessListsMultiplePeers(t *testing.T) {
@@ -2462,7 +2835,7 @@ func TestCatchUpRetriesOnBadBAL(t *testing.T) {
// makeStorageTrieFromSlots builds a storage trie for owner from raw slot
// key->value pairs, using the exact on-disk encoding the flat snapshot and the
-// trie rebuild expect: each leaf is keyed by keccak256(slotKey) and its value is
+// trie generation expect: each leaf is keyed by keccak256(slotKey) and its value is
// rlp(TrimLeftZeroes(value)). Zero-valued slots are skipped (an unset slot has
// no leaf). It returns the storage root, the dirty node set, and the sorted
// snapshot leaves (which a test peer serves verbatim).
@@ -2529,12 +2902,12 @@ func makeStateWithStorageContract(scheme string, plain []*kv, contractAddr commo
// slot, an overwrite of an existing slot, a write of zero (deletion), and a
// multi-tx write where the post-block value wins.
//
-// It fully syncs pivot A (flat-state download + trie rebuild), then moves the
+// It fully syncs pivot A (flat-state download + trie generation), then moves the
// pivot to A+1. The move triggers catchUp, which fetches the A+1 BAL, applies
-// the storage diffs to the flat state, and rebuilds the trie. The rebuild
+// the storage diffs to the flat state, and generates the trie. The generation
// verifies the recomputed root against the pivot's expected post-catch-up root,
// so a successful Sync proves the storage mutations were applied in the exact
-// encoding the trie rebuild consumes. verifyTrie re-walks the result as an
+// encoding the trie generation consumes. verifyTrie re-walks the result as an
// independent confirmation.
func TestCatchUpAppliesStorageBALs(t *testing.T) {
t.Parallel()
@@ -2620,7 +2993,7 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
// Sync, so the follow-up Sync's reorg check sees A as still-canonical and
// runs catchUp instead of resetting. The A+1 header carries the BAL hash
// (verified during catch-up) and the expected post-catch-up state root
- // (verified by the trie rebuild).
+ // (verified by the trie generation).
db := rawdb.NewMemoryDatabase()
numA := uint64(128)
emptyH := common.Hash{}
@@ -2644,7 +3017,7 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
rawdb.WriteHeader(db, hdrB)
rawdb.WriteCanonicalHash(db, hdrB.Hash(), numA+1)
- // Sync 1: full flat-state download + trie rebuild against pivot A.
+ // Sync 1: full flat-state download + trie generation against pivot A.
{
var (
once sync.Once
@@ -2665,7 +3038,7 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
}
close(done)
}
- // Sanity: the rebuilt trie for pivot A is complete and matches rootA. This
+ // Sanity: the generated trie for pivot A is complete and matches rootA. This
// also confirms the test fixture itself is internally consistent.
verifyTrie(scheme, db, rootA, t)
@@ -2690,6 +3063,12 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
if err := syncer.Sync(hdrB, cancel); err != nil {
t.Fatalf("pivot A+1 catch-up sync failed: %v", err)
}
+ // The freeze must re-arm on a pivot-moved cycle too, the downloader
+ // relies on it from download completion until commit, and it must
+ // point at the new pivot the catch-up rolled forward to.
+ if frozen := syncer.FrozenPivot(); frozen == nil || frozen.Hash() != hdrB.Hash() {
+ t.Fatal("pivot not frozen at the new header after catch-up sync")
+ }
close(done)
}
diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go
index 6570d73575..2fefa46492 100644
--- a/eth/tracers/js/tracer_test.go
+++ b/eth/tracers/js/tracer_test.go
@@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
gasLimit uint64 = 31000
startGas uint64 = 10000
value = uint256.NewInt(0)
- contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas, 0), nil)
)
evm.SetTxContext(vmctx.txCtx)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0}
diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go
index decdf588e1..73868d22e0 100644
--- a/eth/tracers/logger/logger_test.go
+++ b/eth/tracers/logger/logger_test.go
@@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) {
var (
logger = NewStructLogger(nil)
evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()})
- contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil)
+ contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000, 0), nil)
)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)}
var index common.Hash
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 1d8573f982..e888acc222 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -839,6 +839,9 @@ type rpcProgress struct {
HealedBytecodeBytes hexutil.Uint64
HealingTrienodes hexutil.Uint64
HealingBytecode hexutil.Uint64
+ SyncedAccessLists hexutil.Uint64
+ TotalAccessLists hexutil.Uint64
+ TrieGenProgress hexutil.Uint64
TxIndexFinishedBlocks hexutil.Uint64
TxIndexRemainingBlocks hexutil.Uint64
StateIndexRemaining hexutil.Uint64
@@ -867,6 +870,9 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
HealedBytecodeBytes: uint64(p.HealedBytecodeBytes),
HealingTrienodes: uint64(p.HealingTrienodes),
HealingBytecode: uint64(p.HealingBytecode),
+ SyncedAccessLists: uint64(p.SyncedAccessLists),
+ TotalAccessLists: uint64(p.TotalAccessLists),
+ TrieGenProgress: uint64(p.TrieGenProgress),
TxIndexFinishedBlocks: uint64(p.TxIndexFinishedBlocks),
TxIndexRemainingBlocks: uint64(p.TxIndexRemainingBlocks),
StateIndexRemaining: uint64(p.StateIndexRemaining),
diff --git a/interfaces.go b/interfaces.go
index 8b3dbe3a42..2f4f53634f 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -127,13 +127,18 @@ type SyncProgress struct {
SyncedStorage uint64 // Number of storage slots downloaded
SyncedStorageBytes uint64 // Number of storage trie bytes persisted to disk
+ // Snap/1 specific fields
HealedTrienodes uint64 // Number of state trie nodes downloaded
HealedTrienodeBytes uint64 // Number of state trie bytes persisted to disk
HealedBytecodes uint64 // Number of bytecodes downloaded
HealedBytecodeBytes uint64 // Number of bytecodes persisted to disk
+ HealingTrienodes uint64 // Number of state trie nodes pending
+ HealingBytecode uint64 // Number of bytecodes pending
- HealingTrienodes uint64 // Number of state trie nodes pending
- HealingBytecode uint64 // Number of bytecodes pending
+ // Snap/2 specific fields
+ SyncedAccessLists uint64 // Number of block access lists fetched during catch-up
+ TotalAccessLists uint64 // Total number of block access lists to fetch for catch-up
+ TrieGenProgress uint64 // Trie generation completion, in percent (0..100)
// "transaction indexing" fields
TxIndexFinishedBlocks uint64 // Number of blocks whose transactions are already indexed
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 22a59aab58..2eb4dee3c0 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -182,6 +182,9 @@ func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) {
"healedBytecodeBytes": hexutil.Uint64(progress.HealedBytecodeBytes),
"healingTrienodes": hexutil.Uint64(progress.HealingTrienodes),
"healingBytecode": hexutil.Uint64(progress.HealingBytecode),
+ "syncedAccessLists": hexutil.Uint64(progress.SyncedAccessLists),
+ "totalAccessLists": hexutil.Uint64(progress.TotalAccessLists),
+ "trieGenProgress": hexutil.Uint64(progress.TrieGenProgress),
"txIndexFinishedBlocks": hexutil.Uint64(progress.TxIndexFinishedBlocks),
"txIndexRemainingBlocks": hexutil.Uint64(progress.TxIndexRemainingBlocks),
"stateIndexRemaining": hexutil.Uint64(progress.StateIndexRemaining),
diff --git a/internal/memlimit/probe.go b/internal/memlimit/probe.go
new file mode 100644
index 0000000000..d5b7652d3b
--- /dev/null
+++ b/internal/memlimit/probe.go
@@ -0,0 +1,59 @@
+// 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 memlimit detects the effective memory limit of the current
+// process. On Linux the cgroup limit is consulted first, with total
+// system memory as the fallback on all platforms.
+package memlimit
+
+import (
+ gopsutil "github.com/shirou/gopsutil/mem"
+)
+
+// Source identifies which mechanism produced the limit value.
+type Source int
+
+const (
+ SourceUnknown Source = iota
+ SourceCgroupV2
+ SourceCgroupV1
+ SourceSystem
+)
+
+func (s Source) String() string {
+ switch s {
+ case SourceCgroupV2:
+ return "cgroup-v2"
+ case SourceCgroupV1:
+ return "cgroup-v1"
+ case SourceSystem:
+ return "system"
+ default:
+ return "unknown"
+ }
+}
+
+// Limit returns the memory limit visible to this process in bytes and
+// the source that produced it.
+func Limit() (bytes uint64, source Source) {
+ if v, src, ok := platformLimit(); ok {
+ return v, src
+ }
+ if mem, err := gopsutil.VirtualMemory(); err == nil {
+ return mem.Total, SourceSystem
+ }
+ return 0, SourceUnknown
+}
diff --git a/internal/memlimit/probe_linux.go b/internal/memlimit/probe_linux.go
new file mode 100644
index 0000000000..58eb9de182
--- /dev/null
+++ b/internal/memlimit/probe_linux.go
@@ -0,0 +1,155 @@
+// 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 .
+
+//go:build linux
+
+package memlimit
+
+import (
+ "os"
+ "path"
+ "strconv"
+ "strings"
+)
+
+// cgroupV1UnlimitedThreshold detects the v1 "no limit" sentinel, which
+// is LONG_MAX rounded down to the kernel page size. Anything above 1<<62
+// is treated as unlimited regardless of page size.
+const cgroupV1UnlimitedThreshold = uint64(1) << 62
+
+// fileReader abstracts os.ReadFile for testing.
+type fileReader func(path string) ([]byte, error)
+
+// platformLimit returns the cgroup limit (v2 memory.max or v1
+// memory.limit_in_bytes) of the current process. The cgroup limit is
+// the authoritative budget in a container, where /proc/meminfo
+// reports the host's RAM.
+func platformLimit() (uint64, Source, bool) {
+ if v, ok := cgroupV2Limit(os.ReadFile); ok {
+ return v, SourceCgroupV2, true
+ }
+ if v, ok := cgroupV1Limit(os.ReadFile); ok {
+ return v, SourceCgroupV1, true
+ }
+ return 0, SourceUnknown, false
+}
+
+// cgroupV2Limit reads the cgroup v2 memory.max for the current process.
+// It probes /sys/fs/cgroup directly first (the effective root inside a
+// cgroup-namespaced container), then the path from /proc/self/cgroup
+// for the bare-metal case where the limit sits on a systemd slice.
+func cgroupV2Limit(read fileReader) (uint64, bool) {
+ if v, ok := readCgroupV2At("/sys/fs/cgroup", "/", read); ok {
+ return v, true
+ }
+ procPath, ok := readProcSelfCgroupV2(read)
+ if !ok || procPath == "/" {
+ return 0, false
+ }
+ return readCgroupV2At("/sys/fs/cgroup", procPath, read)
+}
+
+// readCgroupV2At reads memory.max under root+rel, walking up parents
+// until a numeric value is found or the path bottoms out.
+func readCgroupV2At(root, rel string, read fileReader) (uint64, bool) {
+ // cgroup.controllers exists only on v2; if absent, v2 is not mounted here.
+ if _, err := read(path.Join(root, "cgroup.controllers")); err != nil {
+ return 0, false
+ }
+ for {
+ raw, err := read(path.Join(root, rel, "memory.max"))
+ if err == nil {
+ s := strings.TrimSpace(string(raw))
+ if s != "max" {
+ // Zero is legal to write but degenerate; treat it like
+ // "max" and keep walking up.
+ if n, err := strconv.ParseUint(s, 10, 64); err == nil && n != 0 {
+ return n, true
+ }
+ }
+ }
+ if rel == "/" || rel == "" {
+ return 0, false
+ }
+ rel = path.Dir(rel)
+ }
+}
+
+// readProcSelfCgroupV2 returns the cgroup path from the v2 line
+// ("0::") of /proc/self/cgroup.
+func readProcSelfCgroupV2(read fileReader) (string, bool) {
+ raw, err := read("/proc/self/cgroup")
+ if err != nil {
+ return "", false
+ }
+ for line := range strings.SplitSeq(strings.TrimSpace(string(raw)), "\n") {
+ // v2 unified line: "0::"
+ if strings.HasPrefix(line, "0::") {
+ return strings.TrimPrefix(line, "0::"), true
+ }
+ }
+ return "", false
+}
+
+// cgroupV1Limit reads memory.limit_in_bytes from the v1 memory
+// controller, walking up parents when a node reports the unlimited
+// sentinel.
+func cgroupV1Limit(read fileReader) (uint64, bool) {
+ rel, ok := readProcSelfCgroupV1Memory(read)
+ if !ok {
+ return 0, false
+ }
+ root := "/sys/fs/cgroup/memory"
+ if _, err := read(path.Join(root, "memory.limit_in_bytes")); err != nil {
+ return 0, false
+ }
+ for {
+ raw, err := read(path.Join(root, rel, "memory.limit_in_bytes"))
+ if err == nil {
+ if n, err := strconv.ParseUint(strings.TrimSpace(string(raw)), 10, 64); err == nil {
+ if n != 0 && n < cgroupV1UnlimitedThreshold {
+ return n, true
+ }
+ }
+ }
+ if rel == "/" || rel == "" {
+ return 0, false
+ }
+ rel = path.Dir(rel)
+ }
+}
+
+// readProcSelfCgroupV1Memory parses /proc/self/cgroup for the v1 memory
+// controller line (":memory:" or ":...,memory,...:").
+func readProcSelfCgroupV1Memory(read fileReader) (string, bool) {
+ raw, err := read("/proc/self/cgroup")
+ if err != nil {
+ return "", false
+ }
+ for line := range strings.SplitSeq(strings.TrimSpace(string(raw)), "\n") {
+ // Format: "::"
+ parts := strings.SplitN(line, ":", 3)
+ if len(parts) != 3 {
+ continue
+ }
+ for ctrl := range strings.SplitSeq(parts[1], ",") {
+ if ctrl == "memory" {
+ return parts[2], true
+ }
+ }
+ }
+ return "", false
+}
diff --git a/internal/memlimit/probe_linux_test.go b/internal/memlimit/probe_linux_test.go
new file mode 100644
index 0000000000..51432b9e3e
--- /dev/null
+++ b/internal/memlimit/probe_linux_test.go
@@ -0,0 +1,172 @@
+// 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 .
+
+//go:build linux
+
+package memlimit
+
+import (
+ "os"
+ "testing"
+)
+
+// fakeFS is a fileReader backed by an in-memory map. Missing keys
+// return os.ErrNotExist.
+type fakeFS map[string]string
+
+func (f fakeFS) read(path string) ([]byte, error) {
+ v, ok := f[path]
+ if !ok {
+ return nil, os.ErrNotExist
+ }
+ return []byte(v), nil
+}
+
+func TestCgroupV2Container(t *testing.T) {
+ // Namespaced container (Docker default since 20.10): memory.max
+ // sits directly at the cgroup root.
+ fs := fakeFS{
+ "/sys/fs/cgroup/cgroup.controllers": "memory cpu io",
+ "/sys/fs/cgroup/memory.max": "536870912",
+ "/proc/self/cgroup": "0::/",
+ }
+ bytes, ok := cgroupV2Limit(fs.read)
+ if !ok || bytes != 536870912 {
+ t.Errorf("got (%d, %v), want (536870912, true)", bytes, ok)
+ }
+}
+
+func TestCgroupV2Unlimited(t *testing.T) {
+ fs := fakeFS{
+ "/sys/fs/cgroup/cgroup.controllers": "memory cpu io",
+ "/sys/fs/cgroup/memory.max": "max",
+ "/proc/self/cgroup": "0::/",
+ }
+ if _, ok := cgroupV2Limit(fs.read); ok {
+ t.Errorf("expected ok=false for fully unlimited v2 hierarchy")
+ }
+}
+
+func TestCgroupV2LimitOnAncestor(t *testing.T) {
+ // Bare-metal systemd: the leaf has no limit but the containing
+ // slice does.
+ fs := fakeFS{
+ "/sys/fs/cgroup/cgroup.controllers": "memory cpu io",
+ "/sys/fs/cgroup/memory.max": "max",
+ "/sys/fs/cgroup/system.slice/memory.max": "8589934592",
+ "/sys/fs/cgroup/system.slice/geth.service/memory.max": "max",
+ "/proc/self/cgroup": "0::/system.slice/geth.service",
+ }
+ bytes, ok := cgroupV2Limit(fs.read)
+ if !ok || bytes != 8589934592 {
+ t.Errorf("got (%d, %v), want (8589934592, true)", bytes, ok)
+ }
+}
+
+func TestCgroupV2PrefersDirectRoot(t *testing.T) {
+ // In a namespaced container /proc/self/cgroup may show a host-side
+ // path; the direct probe at the root must win.
+ fs := fakeFS{
+ "/sys/fs/cgroup/cgroup.controllers": "memory cpu io",
+ "/sys/fs/cgroup/memory.max": "536870912",
+ "/sys/fs/cgroup/system.slice/memory.max": "max",
+ "/proc/self/cgroup": "0::/system.slice/docker-abc.scope",
+ }
+ bytes, ok := cgroupV2Limit(fs.read)
+ if !ok || bytes != 536870912 {
+ t.Errorf("got (%d, ok=%v), want (536870912, true)", bytes, ok)
+ }
+}
+
+func TestCgroupV2ZeroWalksUp(t *testing.T) {
+ // memory.max="0" is legal but degenerate; walk up like "max".
+ fs := fakeFS{
+ "/sys/fs/cgroup/cgroup.controllers": "memory cpu io",
+ "/sys/fs/cgroup/memory.max": "max",
+ "/sys/fs/cgroup/system.slice/memory.max": "536870912",
+ "/sys/fs/cgroup/system.slice/geth.service/memory.max": "0",
+ "/proc/self/cgroup": "0::/system.slice/geth.service",
+ }
+ bytes, ok := cgroupV2Limit(fs.read)
+ if !ok || bytes != 536870912 {
+ t.Errorf("got (%d, ok=%v), want (536870912, true)", bytes, ok)
+ }
+}
+
+func TestCgroupV1(t *testing.T) {
+ fs := fakeFS{
+ // no v2 hallmark file
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes": "9223372036854771712",
+ "/sys/fs/cgroup/memory/docker/abc/memory.limit_in_bytes": "1073741824",
+ "/proc/self/cgroup": "12:memory:/docker/abc\n11:cpu:/docker/abc",
+ }
+ if _, ok := cgroupV2Limit(fs.read); ok {
+ t.Errorf("expected v2 probe to fail on a v1-only host")
+ }
+ bytes, ok := cgroupV1Limit(fs.read)
+ if !ok || bytes != 1073741824 {
+ t.Errorf("got (%d, %v), want (1073741824, true)", bytes, ok)
+ }
+}
+
+func TestCgroupV1Unlimited(t *testing.T) {
+ fs := fakeFS{
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes": "9223372036854771712",
+ "/sys/fs/cgroup/memory/docker/abc/memory.limit_in_bytes": "9223372036854771712",
+ "/proc/self/cgroup": "12:memory:/docker/abc",
+ }
+ if _, ok := cgroupV1Limit(fs.read); ok {
+ t.Errorf("expected ok=false when v1 reports the unlimited sentinel everywhere")
+ }
+}
+
+func TestCgroupV1NonDefaultPageSize(t *testing.T) {
+ // The unlimited sentinel is LONG_MAX aligned to the local page
+ // size, so it differs on 16 KiB and 64 KiB page kernels.
+ fs := fakeFS{
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes": "9223372036854710272", // 64 KiB-page sentinel
+ "/sys/fs/cgroup/memory/foo/memory.limit_in_bytes": "9223372036854767616", // 16 KiB-page sentinel
+ "/proc/self/cgroup": "12:memory:/foo",
+ }
+ if _, ok := cgroupV1Limit(fs.read); ok {
+ t.Errorf("expected ok=false: both values are page-aligned LONG_MAX")
+ }
+}
+
+func TestCgroupV1CombinedControllers(t *testing.T) {
+ // Some kernels list multiple controllers per line.
+ fs := fakeFS{
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes": "9223372036854771712",
+ "/sys/fs/cgroup/memory/foo/memory.limit_in_bytes": "2147483648",
+ "/proc/self/cgroup": "8:cpu,memory,blkio:/foo",
+ }
+ bytes, ok := cgroupV1Limit(fs.read)
+ if !ok || bytes != 2147483648 {
+ t.Errorf("got (%d, ok=%v), want (2147483648, true)", bytes, ok)
+ }
+}
+
+func TestNoCgroup(t *testing.T) {
+ fs := fakeFS{
+ "/proc/self/cgroup": "0::/",
+ }
+ if _, ok := cgroupV2Limit(fs.read); ok {
+ t.Errorf("expected v2 ok=false when v2 is not mounted")
+ }
+ if _, ok := cgroupV1Limit(fs.read); ok {
+ t.Errorf("expected v1 ok=false when v1 is not mounted")
+ }
+}
diff --git a/internal/memlimit/probe_other.go b/internal/memlimit/probe_other.go
new file mode 100644
index 0000000000..56d0122349
--- /dev/null
+++ b/internal/memlimit/probe_other.go
@@ -0,0 +1,25 @@
+// 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 .
+
+//go:build !linux
+
+package memlimit
+
+// platformLimit reports no platform-specific limit on non-Linux
+// systems; the caller falls back to total system memory.
+func platformLimit() (uint64, Source, bool) {
+ return 0, SourceUnknown, false
+}
diff --git a/internal/memlimit/probe_test.go b/internal/memlimit/probe_test.go
new file mode 100644
index 0000000000..b86e95629a
--- /dev/null
+++ b/internal/memlimit/probe_test.go
@@ -0,0 +1,28 @@
+// 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 memlimit
+
+import "testing"
+
+// TestLimitSmoke asserts that Limit() returns a non-zero value,
+// exercising the real probe and the gopsutil fallback.
+func TestLimitSmoke(t *testing.T) {
+ bytes, src := Limit()
+ if bytes == 0 {
+ t.Errorf("Limit() returned 0 bytes (source=%s); expected non-zero on any sane host", src)
+ }
+}
diff --git a/log/format.go b/log/format.go
index e7dd8a4099..833474e829 100644
--- a/log/format.go
+++ b/log/format.go
@@ -123,7 +123,7 @@ func FormatSlogValue(v slog.Value, tmp []byte) (result []byte) {
var value any
defer func() {
if err := recover(); err != nil {
- if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
+ if v := reflect.ValueOf(value); v.Kind() == reflect.Pointer && v.IsNil() {
result = []byte("")
} else {
panic(err)
diff --git a/miner/payload_building.go b/miner/payload_building.go
index a2cc8df9d0..c4322e3317 100644
--- a/miner/payload_building.go
+++ b/miner/payload_building.go
@@ -341,7 +341,7 @@ func (payload *Payload) updateSpanForDelivery(bSpan trace.Span) {
// BuildTestingPayload is for testing_buildBlockV*. It creates a block with the exact content given
// by the parameters instead of using the locally available transactions.
-func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*engine.ExecutionPayloadEnvelope, error) {
+func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*types.Block, *engine.ExecutionPayloadEnvelope, error) {
fullParams := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
@@ -358,7 +358,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
}
res := miner.generateWork(context.Background(), fullParams, false)
if res.err != nil {
- return nil, res.err
+ return nil, nil, res.err
}
- return engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
+ return res.block, engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
}
diff --git a/p2p/discover/lookup.go b/p2p/discover/lookup.go
index 416256fb36..c719333235 100644
--- a/p2p/discover/lookup.go
+++ b/p2p/discover/lookup.go
@@ -250,7 +250,7 @@ func (it *lookupIterator) slowdown() {
if diff > minInterval {
return
}
- wait := time.NewTimer(diff)
+ wait := time.NewTimer(minInterval - diff)
defer wait.Stop()
select {
case <-wait.C:
diff --git a/p2p/discover/table.go b/p2p/discover/table.go
index 016a2d1af3..ca29e51979 100644
--- a/p2p/discover/table.go
+++ b/p2p/discover/table.go
@@ -290,28 +290,33 @@ func (tab *Table) refresh() <-chan struct{} {
// preferLive is true and the table contains any verified nodes, the result will not
// contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes.
-func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
+func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) (nodes nodesByDistance) {
+ nodes.target = target
tab.mutex.Lock()
defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many
// buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults).
- nodes := &nodesByDistance{target: target}
- liveNodes := &nodesByDistance{target: target}
- for _, b := range &tab.buckets {
- for _, n := range b.entries {
- nodes.push(n.Node, nresults)
- if preferLive && n.isValidatedLive {
- liveNodes.push(n.Node, nresults)
+ if preferLive {
+ for _, b := range &tab.buckets {
+ for _, n := range b.entries {
+ if n.isValidatedLive {
+ nodes.push(n.Node, nresults)
+ }
}
}
+ if len(nodes.entries) > 0 {
+ return
+ }
}
- if preferLive && len(liveNodes.entries) > 0 {
- return liveNodes
+ for _, b := range &tab.buckets {
+ for _, n := range b.entries {
+ nodes.push(n.Node, nresults)
+ }
}
- return nodes
+ return
}
// appendBucketNodes adds nodes at the given distance to the result slice.
diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go
index a16b4d9cab..fcc4697047 100644
--- a/p2p/discover/table_test.go
+++ b/p2p/discover/table_test.go
@@ -597,3 +597,77 @@ func newkey() *ecdsa.PrivateKey {
}
return key
}
+
+// BenchmarkTable_findnodeByID exercises findnodeByID across table sizes, result
+// counts, and liveness ratios. The _PreferLive_NoLive cases cover the fallback
+// path where preferLive is requested but no validated-live nodes exist.
+func BenchmarkTable_findnodeByID(b *testing.B) {
+ benchmarks := []struct {
+ name string
+ tableSize int
+ nresults int
+ preferLive bool
+ liveRatio float64 // fraction of nodes marked validated-live
+ }{
+ {"SmallTable_5Results_NoPreferLive", 50, 5, false, 0.0},
+ {"SmallTable_16Results_NoPreferLive", 50, 16, false, 0.0},
+ {"SmallTable_5Results_PreferLive_AllLive", 50, 5, true, 1.0},
+ {"SmallTable_16Results_PreferLive_AllLive", 50, 16, true, 1.0},
+ {"SmallTable_5Results_PreferLive_HalfLive", 50, 5, true, 0.5},
+ {"SmallTable_16Results_PreferLive_HalfLive", 50, 16, true, 0.5},
+ {"SmallTable_5Results_PreferLive_NoLive", 50, 5, true, 0.0},
+ {"SmallTable_16Results_PreferLive_NoLive", 50, 16, true, 0.0},
+
+ {"MediumTable_5Results_NoPreferLive", 200, 5, false, 0.0},
+ {"MediumTable_16Results_NoPreferLive", 200, 16, false, 0.0},
+ {"MediumTable_5Results_PreferLive_AllLive", 200, 5, true, 1.0},
+ {"MediumTable_16Results_PreferLive_AllLive", 200, 16, true, 1.0},
+ {"MediumTable_5Results_PreferLive_HalfLive", 200, 5, true, 0.5},
+ {"MediumTable_16Results_PreferLive_HalfLive", 200, 16, true, 0.5},
+ {"MediumTable_5Results_PreferLive_NoLive", 200, 5, true, 0.0},
+ {"MediumTable_16Results_PreferLive_NoLive", 200, 16, true, 0.0},
+
+ {"FullTable_5Results_NoPreferLive", nBuckets * bucketSize, 5, false, 0.0},
+ {"FullTable_16Results_NoPreferLive", nBuckets * bucketSize, 16, false, 0.0},
+ {"FullTable_5Results_PreferLive_AllLive", nBuckets * bucketSize, 5, true, 1.0},
+ {"FullTable_16Results_PreferLive_AllLive", nBuckets * bucketSize, 16, true, 1.0},
+ {"FullTable_5Results_PreferLive_HalfLive", nBuckets * bucketSize, 5, true, 0.5},
+ {"FullTable_16Results_PreferLive_HalfLive", nBuckets * bucketSize, 16, true, 0.5},
+ {"FullTable_5Results_PreferLive_NoLive", nBuckets * bucketSize, 5, true, 0.0},
+ {"FullTable_16Results_PreferLive_NoLive", nBuckets * bucketSize, 16, true, 0.0},
+ }
+
+ for _, bm := range benchmarks {
+ b.Run(bm.name, func(b *testing.B) {
+ tab, db := newTestTable(newPingRecorder(), Config{})
+ defer db.Close()
+ defer tab.close()
+ <-tab.initDone
+
+ self := tab.self().ID()
+ nodes := make([]*enode.Node, bm.tableSize)
+ for i := range nodes {
+ // Spread across buckets by varying log-distance in the valid range.
+ d := bucketMinDistance + 1 + i%nBuckets
+ nodes[i] = nodeAtDistance(self, d, intIP(i))
+ }
+
+ liveCount := int(float64(len(nodes)) * bm.liveRatio)
+ if liveCount > 0 {
+ fillTable(tab, nodes[:liveCount], true)
+ }
+ if liveCount < len(nodes) {
+ fillTable(tab, nodes[liveCount:], false)
+ }
+
+ var target enode.ID
+ rand.Read(target[:])
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ _ = tab.findnodeByID(target, bm.nresults, bm.preferLive)
+ }
+ })
+ }
+}
diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go
index ae8cbec3e2..28702f0fc6 100644
--- a/p2p/discover/v4_udp.go
+++ b/p2p/discover/v4_udp.go
@@ -355,7 +355,10 @@ func (t *UDPv4) findnode(toid enode.ID, toAddrPort netip.AddrPort, target v4wire
// RequestENR sends ENRRequest to the given node and waits for a response.
func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
- addr, _ := n.UDPEndpoint()
+ addr, ok := n.UDPEndpoint()
+ if !ok {
+ return nil, errNoUDPEndpoint
+ }
t.ensureBond(n.ID(), addr)
req := &v4wire.ENRRequest{
diff --git a/p2p/discover/v4_udp_test.go b/p2p/discover/v4_udp_test.go
index 287f0c34fa..da86bb0e06 100644
--- a/p2p/discover/v4_udp_test.go
+++ b/p2p/discover/v4_udp_test.go
@@ -158,6 +158,23 @@ func TestUDPv4_pingTimeout(t *testing.T) {
}
}
+// TestUDPv4_RequestENRNoUDPEndpoint verifies that RequestENR returns a clean
+// errNoUDPEndpoint error for a node without a usable UDP endpoint, instead of
+// silently sending a packet to the zero address and waiting for a timeout.
+// This mirrors the existing guards in ping/Ping/findnode.
+func TestUDPv4_RequestENRNoUDPEndpoint(t *testing.T) {
+ t.Parallel()
+ test := newUDPTest(t)
+ defer test.close()
+
+ key := newkey()
+ // UDP port 0 makes UDPEndpoint return ok=false.
+ node := enode.NewV4(&key.PublicKey, net.ParseIP("1.2.3.4"), 2222, 0)
+ if _, err := test.udp.RequestENR(node); !errors.Is(err, errNoUDPEndpoint) {
+ t.Errorf("expected errNoUDPEndpoint, got %v", err)
+ }
+}
+
type testPacket byte
func (req testPacket) Kind() byte { return byte(req) }
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 3e36b83547..69e10fa5d9 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -88,6 +88,7 @@ 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.
@@ -100,6 +101,7 @@ 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
// These have been changed during the course of the chain
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction.
@@ -196,6 +198,12 @@ const (
// the bound has a small safety margin for system-contract accesses that
// don't consume block gas.
BALItemCost uint64 = 2000
+
+ AccountCreationSize = 120
+ StorageCreationSize = 64
+ AuthorizationCreationSize = 23
+ CostPerStateByte = 1530
+ SystemMaxSStoresPerCall = 16
)
// Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation
diff --git a/rlp/decode.go b/rlp/decode.go
index 19074072fb..881257cdfb 100644
--- a/rlp/decode.go
+++ b/rlp/decode.go
@@ -166,7 +166,7 @@ func makeDecoder(typ reflect.Type, tags rlpstruct.Tags) (dec decoder, err error)
return decodeU256, nil
case typ == u256Int:
return decodeU256NoPtr, nil
- case kind == reflect.Ptr:
+ case kind == reflect.Pointer:
return makePtrDecoder(typ, tags)
case reflect.PointerTo(typ).Implements(decoderInterface):
return decodeDecoder, nil
@@ -936,7 +936,7 @@ func (s *Stream) Decode(val interface{}) error {
}
rval := reflect.ValueOf(val)
rtyp := rval.Type()
- if rtyp.Kind() != reflect.Ptr {
+ if rtyp.Kind() != reflect.Pointer {
return errNoPointer
}
if rval.IsNil() {
diff --git a/rlp/encode.go b/rlp/encode.go
index 9d04e6324a..4a89b5f730 100644
--- a/rlp/encode.go
+++ b/rlp/encode.go
@@ -173,7 +173,7 @@ func makeWriter(typ reflect.Type, ts rlpstruct.Tags) (writer, error) {
return writeU256IntPtr, nil
case typ == u256Int:
return writeU256IntNoPtr, nil
- case kind == reflect.Ptr:
+ case kind == reflect.Pointer:
return makePtrWriter(typ, ts)
case reflect.PointerTo(typ).Implements(encoderInterface):
return makeEncoderWriter(typ), nil
diff --git a/rlp/internal/rlpstruct/rlpstruct.go b/rlp/internal/rlpstruct/rlpstruct.go
index 2e3eeb6881..5b448d37ed 100644
--- a/rlp/internal/rlpstruct/rlpstruct.go
+++ b/rlp/internal/rlpstruct/rlpstruct.go
@@ -156,7 +156,7 @@ func parseTag(field Field, lastPublic int) (Tags, error) {
ts.Ignored = true
case "nil", "nilString", "nilList":
ts.NilOK = true
- if field.Type.Kind != reflect.Ptr {
+ if field.Type.Kind != reflect.Pointer {
return ts, TagError{Field: name, Tag: t, Err: "field is not a pointer"}
}
switch t {
diff --git a/rlp/rlpgen/types.go b/rlp/rlpgen/types.go
index ea7dc96d88..c73ea6d188 100644
--- a/rlp/rlpgen/types.go
+++ b/rlp/rlpgen/types.go
@@ -47,7 +47,7 @@ func typeReflectKind(typ types.Type) reflect.Kind {
case *types.Map:
return reflect.Map
case *types.Pointer:
- return reflect.Ptr
+ return reflect.Pointer
case *types.Signature:
return reflect.Func
case *types.Slice:
diff --git a/rlp/typecache.go b/rlp/typecache.go
index eebf4cd611..1cc16952d1 100644
--- a/rlp/typecache.go
+++ b/rlp/typecache.go
@@ -203,7 +203,7 @@ func rtypeToStructType(typ reflect.Type, rec map[reflect.Type]*rlpstruct.Type) *
IsDecoder: typ.Implements(decoderInterface),
}
rec[typ] = t
- if k == reflect.Array || k == reflect.Slice || k == reflect.Ptr {
+ if k == reflect.Array || k == reflect.Slice || k == reflect.Pointer {
t.Elem = rtypeToStructType(typ.Elem(), rec)
}
return t
diff --git a/rpc/client.go b/rpc/client.go
index 51a595e726..c73459e7e1 100644
--- a/rpc/client.go
+++ b/rpc/client.go
@@ -335,7 +335,7 @@ func (c *Client) Call(result interface{}, method string, args ...interface{}) er
// The result must be a pointer so that package json can unmarshal into it. You
// can also pass nil, in which case the result is ignored.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
- if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
+ if result != nil && reflect.TypeOf(result).Kind() != reflect.Pointer {
return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
}
msg, err := c.newMessage(method, args...)
diff --git a/rpc/json.go b/rpc/json.go
index b2a961d109..f0e82afb5c 100644
--- a/rpc/json.go
+++ b/rpc/json.go
@@ -407,7 +407,7 @@ func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]
}
// Set any missing args to nil.
for i := len(args); i < len(types); i++ {
- if types[i].Kind() != reflect.Ptr {
+ if types[i].Kind() != reflect.Pointer {
return nil, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, reflect.Zero(types[i]))
@@ -425,7 +425,7 @@ func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Valu
if err := dec.Decode(argval.Interface()); err != nil {
return args, fmt.Errorf("invalid argument %d: %v", i, err)
}
- if argval.IsNil() && types[i].Kind() != reflect.Ptr {
+ if argval.IsNil() && types[i].Kind() != reflect.Pointer {
return args, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, argval.Elem())
diff --git a/rpc/service.go b/rpc/service.go
index b64f43b82d..d75eb02383 100644
--- a/rpc/service.go
+++ b/rpc/service.go
@@ -225,7 +225,7 @@ func isErrorType(t reflect.Type) bool {
// Is t Subscription or *Subscription?
func isSubscriptionType(t reflect.Type) bool {
- for t.Kind() == reflect.Ptr {
+ for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t == subscriptionType
diff --git a/rpc/tracing_test.go b/rpc/tracing_test.go
index fa2ef09e76..dafa9fb8a7 100644
--- a/rpc/tracing_test.go
+++ b/rpc/tracing_test.go
@@ -346,17 +346,27 @@ func TestTracingSubscribeUnsubscribe(t *testing.T) {
// like notifications (no "id" field).
func postJSONRPC(t *testing.T, url, body string) {
t.Helper()
+ if err := tryPostJSONRPC(url, body); err != nil {
+ t.Fatalf("request: %v", err)
+ }
+}
+
+// tryPostJSONRPC is like postJSONRPC but returns the transport error instead of
+// failing the test. The write-timeout test uses this because the HTTP
+// WriteTimeout can drop the connection before the response is flushed.
+func tryPostJSONRPC(url, body string) error {
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
if err != nil {
- t.Fatalf("new request: %v", err)
+ return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
- t.Fatalf("request: %v", err)
+ return err
}
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
+ return nil
}
// TestTracingHTTPNotification verifies that a JSON-RPC notification emits the
@@ -630,7 +640,12 @@ func TestTracingHTTPTimeout(t *testing.T) {
// test_block waits on ctx.Done() and returns an error. The internal
// timer cancels ctx, so test_block unblocks shortly after the timeout
// response goes out.
- postJSONRPC(t, httpsrv.URL, `{"jsonrpc":"2.0","id":1,"method":"test_block"}`)
+ //
+ // Ignore the client-side result. Under load the HTTP WriteTimeout can
+ // drop the connection before the timeout response is flushed, which the
+ // client sees as EOF. The server still records the timeout on its span,
+ // which is what we assert below.
+ _ = tryPostJSONRPC(httpsrv.URL, `{"jsonrpc":"2.0","id":1,"method":"test_block"}`)
// Wait for the in-flight request to finish so the deferred spanEnd fires
// before GetSpans is called.
diff --git a/tests/state_test.go b/tests/state_test.go
index cf1d4bce4c..09c5cad40e 100644
--- a/tests/state_test.go
+++ b/tests/state_test.go
@@ -325,10 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StartTimer()
start := time.Now()
- initialGas := vm.NewGasBudget(msg.GasLimit)
+ initialGas := vm.NewGasBudget(msg.GasLimit, 0)
// Execute the message.
- _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value)
+ _, result, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas, msg.Value)
if err != nil {
b.Error(err)
return
@@ -337,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StopTimer()
elapsed += uint64(time.Since(start))
refund += state.StateDB.GetRefund()
- gasUsed += leftOverGas.Used(initialGas)
+ gasUsed += result.Used(initialGas)
state.StateDB.RevertToSnapshot(snapshot)
}
diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go
index 572c109f1e..91f7d6c3ec 100644
--- a/tests/transaction_test_util.go
+++ b/tests/transaction_test_util.go
@@ -80,8 +80,8 @@ func (tt *TransactionTest) Run() error {
if err != nil {
return
}
- // Intrinsic gas
- cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
+ // Intrinsic cost
+ cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte)
if err != nil {
return
}
diff --git a/triedb/generate.go b/triedb/generate.go
index b6906f4c63..1dbc0c2f69 100644
--- a/triedb/generate.go
+++ b/triedb/generate.go
@@ -65,6 +65,21 @@ const (
partitionFinished = ^uint64(0)
)
+// genCounters bundles the progress counters threaded through a GenerateTrie run.
+type genCounters struct {
+ accounts atomic.Int64 // accounts scanned
+ slots atomic.Int64 // storage slots scanned
+ accountUpdated atomic.Int64 // accounts whose stale storage Root was rewritten
+ storageDeleted atomic.Int64 // dangling storage slots removed
+
+ accountTrieNodes atomic.Int64 // generated account trie nodes
+ accountTrieBytes atomic.Int64 // generated account trie bytes
+ storageTrieNodes atomic.Int64 // generated storage trie nodes
+ storageTrieBytes atomic.Int64 // generated storage trie bytes
+
+ progress [numPartitions]atomic.Uint64 // per-partition keyspace position
+}
+
// rangeIterators bundles the per-partition account and storage iterators.
type rangeIterators struct {
db ethdb.Database
@@ -134,7 +149,7 @@ func reopenFlatIterator(db ethdb.Database, old *internal.HoldableIterator, prefi
// both per-account storage subtries and the partition's slice of the
// account trie. Returns the partition's stripped subtree root blob, or
// nil if the partition had no accounts at all.
-func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Database, scheme string, partition byte, rangeStart, rangeEnd common.Hash, scanned, updated, deleted *atomic.Int64, pos *atomic.Uint64) ([]byte, error) {
+func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Database, scheme string, partition byte, rangeStart, rangeEnd common.Hash, c *genCounters) ([]byte, error) {
iters := openRangeIterators(db, rangeStart)
defer iters.release()
@@ -154,6 +169,13 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if len(path) == 1 {
root = common.CopyBytes(blob)
}
+ c.accountTrieNodes.Add(1)
+
+ if scheme == rawdb.PathScheme {
+ c.accountTrieBytes.Add(int64(len(path) + len(blob)))
+ } else {
+ c.accountTrieBytes.Add(int64(common.HashLength + len(blob)))
+ }
rawdb.WriteTrieNode(batch, common.Hash{}, path, hash, blob, scheme)
})
@@ -172,8 +194,8 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if bytes.Compare(accountHash[:], rangeEnd[:]) > 0 {
break
}
- scanned.Add(1)
- pos.Store(binary.BigEndian.Uint64(accountHash[:8]))
+ c.accounts.Add(1)
+ c.progress[partition].Store(binary.BigEndian.Uint64(accountHash[:8]))
// Decode the account object
account, err := types.FullAccount(iters.acct.Value())
@@ -184,6 +206,13 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
// Build the account's storage trie from the flat storage snapshot.
// StackTrie's onTrieNode callback persists nodes as they finalize.
storageTrie := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) {
+ c.storageTrieNodes.Add(1)
+
+ if scheme == rawdb.PathScheme {
+ c.storageTrieBytes.Add(int64(len(path) + common.HashLength + len(blob)))
+ } else {
+ c.storageTrieBytes.Add(int64(common.HashLength + len(blob)))
+ }
rawdb.WriteTrieNode(batch, accountHash, path, hash, blob, scheme)
})
@@ -213,7 +242,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
copy(lastDanglingAccount, storageAccount)
log.Error("Unexpected storage entries for dangling account", "expected", accountHash, "got", common.BytesToHash(storageAccount))
}
- deleted.Add(1)
+ c.storageDeleted.Add(1)
slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]
rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(storageAccount), common.BytesToHash(slotHash))
if err := iters.flushIfFull(batch, "dangling"); err != nil {
@@ -237,6 +266,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if err := storageTrie.Update(slotHash, iters.stor.Value()); err != nil {
return nil, fmt.Errorf("storage stack trie update for %x: %w", accountHash, err)
}
+ c.slots.Add(1)
if err := iters.flushIfFull(batch, "storage"); err != nil {
return nil, err
}
@@ -252,7 +282,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if computed != account.Root {
account.Root = computed
rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(*account))
- updated.Add(1)
+ c.accountUpdated.Add(1)
}
fullAccount, err := rlp.EncodeToBytes(account)
if err != nil {
@@ -291,7 +321,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
copy(lastDanglingTail, acct)
log.Error("Unexpected storage entries for dangling account", "addrhash", common.BytesToHash(acct))
}
- deleted.Add(1)
+ c.storageDeleted.Add(1)
slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]
rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(acct), common.BytesToHash(slotHash))
if err := iters.flushIfFull(batch, "dangling tail"); err != nil {
@@ -337,7 +367,7 @@ func hashRanges(total int) [][2]common.Hash {
return ranges
}
-// GenerateTrie rebuilds all tries (storage + account) from flat snapshot
+// GenerateTrie builds all tries (storage + account) from flat snapshot
// data in the database. The account hash space is partitioned into 16
// slices aligned with the first-nibble branching of the MPT root. Each
// partition is processed by its own goroutine, which walks its slice,
@@ -346,29 +376,28 @@ func hashRanges(total int) [][2]common.Hash {
// trie. Once every partition has produced its subtree root, the top-level
// branch is assembled and its hash verified against the expected root.
//
-// Resume: on entry, any partition that has a "done" marker from a
-// previous run is skipped. Its subtree blob is read from the marker
-// and handed to assembleRoot directly. On a mid-run crash, only the
-// in-flight partition(s) are redone.
+// Generation is all or nothing: an interrupted run leaves no resume
+// state and the next run builds every partition from scratch.
func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-chan struct{}) (GenerateStats, error) {
+ return GenerateTrieWithProgress(db, scheme, root, cancel, nil)
+}
+
+// GenerateTrieWithProgress is GenerateTrie with live progress reporting.
+func GenerateTrieWithProgress(db ethdb.Database, scheme string, root common.Hash, cancel <-chan struct{}, prog *atomic.Uint64) (GenerateStats, error) {
var (
start = time.Now()
- scanned atomic.Int64
- updated atomic.Int64
- deleted atomic.Int64
- progress [numPartitions]atomic.Uint64
+ c genCounters
progressDone = make(chan struct{})
// partitionBlobs[i] holds the root node for partition i, or nil if
// the partition is empty.
partitionBlobs [numPartitions][]byte
)
- go tickProgress(progressDone, start, &scanned, &updated, &progress)
+ go tickProgress(progressDone, start, &c, prog)
defer close(progressDone)
- // For each partition, either skip (prior done marker found) or run
- // it. Prior runs can leave the partition's raw root blob in the done
- // marker. We recover it here so assembleRoot has everything it needs.
+ // Run every partition concurrently, each producing the subtree root
+ // blob that assembleRoot needs.
var (
ranges = hashRanges(numPartitions)
eg, ctx = errgroup.WithContext(context.Background())
@@ -376,26 +405,16 @@ func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-c
for i, r := range ranges {
partition := byte(i)
rangeStart, rangeEnd := r[0], r[1]
- if blob, ok := rawdb.ReadGenerateTriePartitionDone(db, partition); ok {
- partitionBlobs[partition] = blob
- progress[partition].Store(partitionFinished)
- continue
- }
eg.Go(func() error {
start := time.Now()
- blob, err := generatePartition(ctx, cancel, db, scheme, partition, rangeStart, rangeEnd, &scanned, &updated, &deleted, &progress[partition])
+ blob, err := generatePartition(ctx, cancel, db, scheme, partition, rangeStart, rangeEnd, &c)
if err != nil {
return err
}
log.Info("Partition done", "partition", partition, "elapsed", common.PrettyDuration(time.Since(start)))
- progress[partition].Store(partitionFinished)
+ c.progress[partition].Store(partitionFinished)
partitionBlobs[partition] = blob
-
- // Record completion only after the partition's batch has
- // flushed inside generatePartition, so this marker appears
- // on disk only when every write the partition did is durable.
- rawdb.WriteGenerateTriePartitionDone(db, partition, blob)
return nil
})
}
@@ -404,10 +423,11 @@ func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-c
if err := eg.Wait(); err != nil {
return GenerateStats{}, err
}
-
- // Assemble the top-level root from the partition blobs, verify it
- // matches the expected root, and clear all partition markers on
- // success.
+ if prog != nil {
+ prog.Store(100)
+ }
+ // Assemble the top-level root from the partition blobs and verify it
+ // matches the expected root.
got, err := assembleRoot(db, scheme, partitionBlobs)
if err != nil {
return GenerateStats{}, fmt.Errorf("assemble root: %w", err)
@@ -415,20 +435,17 @@ func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-c
if got != root {
return GenerateStats{}, fmt.Errorf("state root mismatch: got %x, want %x", got, root)
}
+ log.Info("Generated state trie",
+ "accounts", c.accounts.Load(), "slots", c.slots.Load(),
+ "account-nodes", c.accountTrieNodes.Load(), "storage-nodes", c.storageTrieNodes.Load(),
+ "account-nodebytes", common.StorageSize(c.accountTrieBytes.Load()), "storage-nodebytes", common.StorageSize(c.storageTrieBytes.Load()),
+ "updated-accounts", c.accountUpdated.Load(), "dangling-slots", c.storageDeleted.Load(),
+ "elapsed", common.PrettyDuration(time.Since(start)))
- // Clear the partition progress marker, ending the generation process.
- batch := db.NewBatch()
- for i := range numPartitions {
- rawdb.DeleteGenerateTriePartitionDone(batch, byte(i))
- }
- if err := batch.Write(); err != nil {
- return GenerateStats{}, fmt.Errorf("clear partition markers: %w", err)
- }
- log.Info("Generated state trie", "scanned", scanned.Load(), "updated", updated.Load(), "dangling-slots", deleted.Load(), "elapsed", common.PrettyDuration(time.Since(start)))
return GenerateStats{
- Scanned: scanned.Load(),
- Updated: updated.Load(),
- Deleted: deleted.Load(),
+ Scanned: c.accounts.Load(),
+ Updated: c.accountUpdated.Load(),
+ Deleted: c.storageDeleted.Load(),
}, nil
}
@@ -506,25 +523,32 @@ func assembleRoot(db ethdb.Database, scheme string, partitionBlobs [numPartition
// tickProgress logs an aggregate progress line every 30 seconds until done
// is closed. Cheap: a handful of atomic loads and one log line per tick.
-func tickProgress(done <-chan struct{}, start time.Time, scanned, updated *atomic.Int64, progress *[numPartitions]atomic.Uint64) {
+func tickProgress(done <-chan struct{}, start time.Time, c *genCounters, prog *atomic.Uint64) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
+
for {
select {
case <-done:
return
case <-ticker.C:
elapsed := time.Since(start)
- fraction := progressFraction(progress)
+ fraction := progressFraction(&c.progress)
+
+ // Notify the external subscriber about the generation progress
+ if prog != nil {
+ prog.Store(uint64(100 * fraction))
+ }
eta := "n/a"
if fraction > 0.005 {
eta = common.PrettyDuration(time.Duration(float64(elapsed) * (1.0/fraction - 1.0))).String()
}
log.Info("Generating trie",
"progress", fmt.Sprintf("%.1f%%", fraction*100), "eta", eta,
- "scanned", scanned.Load(), "updated", updated.Load(),
+ "accounts", c.accounts.Load(), "slots", c.slots.Load(),
+ "account-updated", c.accountUpdated.Load(), "dangling-slots", c.storageDeleted.Load(),
"elapsed", common.PrettyDuration(elapsed),
- "acct/s", uint64(float64(scanned.Load())/elapsed.Seconds()))
+ "acct/s", uint64(float64(c.accounts.Load())/elapsed.Seconds()))
}
}
}
diff --git a/triedb/generate_test.go b/triedb/generate_test.go
index 2a7d98bfaf..a7dbf1c81a 100644
--- a/triedb/generate_test.go
+++ b/triedb/generate_test.go
@@ -21,7 +21,6 @@ import (
"context"
"math/big"
"sort"
- "sync/atomic"
"testing"
"github.com/ethereum/go-ethereum/common"
@@ -370,97 +369,6 @@ func TestGenerateTrieOrphanStorage(t *testing.T) {
}
}
-// TestGenerateTriePartialResume proves that the resume path actually
-// fires when a partition's done marker is present.
-func TestGenerateTriePartialResume(t *testing.T) {
- // Build the account set. Empty storage keeps the test focused on the
- // account-trie resume path.
- const n = 200
- accounts := make([]testAccount, 0, n)
- for i := 0; i < n; i++ {
- addr := common.BytesToAddress([]byte{byte(i >> 8), byte(i)})
- hash := crypto.Keccak256Hash(addr[:])
- accounts = append(accounts, testAccount{
- hash: hash,
- account: types.StateAccount{
- Nonce: uint64(i),
- Balance: uint256.NewInt(uint64(i + 1)),
- Root: types.EmptyRootHash,
- CodeHash: types.EmptyCodeHash.Bytes(),
- },
- })
- }
- expectedRoot := buildExpectedRoot(t, accounts)
-
- for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
- t.Run(scheme, func(t *testing.T) {
- db := rawdb.NewMemoryDatabase()
-
- // Step 1: write the account snapshots for this run.
- for _, a := range accounts {
- rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account))
- }
-
- // Step 2: run every partition once to populate trie nodes on disk
- // and capture each partition's raw root blob.
- var (
- scanned atomic.Int64
- updated atomic.Int64
- deleted atomic.Int64
- )
- ranges := hashRanges(numPartitions)
- blobs := make([][]byte, numPartitions)
- for i, r := range ranges {
- var pos atomic.Uint64
- blob, err := generatePartition(context.Background(), nil, db, scheme, byte(i), r[0], r[1], &scanned, &updated, &deleted, &pos)
- if err != nil {
- t.Fatalf("pre-run partition %d: %v", i, err)
- }
- blobs[i] = blob
- }
-
- // Step 3: pre-seed done markers for even partitions only.
- for i := 0; i < numPartitions; i++ {
- if i%2 == 0 {
- rawdb.WriteGenerateTriePartitionDone(db, byte(i), blobs[i])
- }
- }
-
- // Step 4: delete flat-state account snapshots for every account that
- // lives in an even partition. After this, rerunning generatePartition for
- // an even partition would find no accounts and produce a nil blob,
- // so a correct final root requires the resume path.
- numDeleted := 0
- for _, a := range accounts {
- if (a.hash[0]>>4)%2 == 0 {
- rawdb.DeleteAccountSnapshot(db, a.hash)
- numDeleted++
- }
- }
- if numDeleted == 0 {
- t.Fatal("test setup failure: no accounts fell in even partitions")
- }
-
- // Step 5: run GenerateTrie. Success implies resume actually consulted
- // the markers. Without it, even partitions would yield nil blobs and
- // the root check inside GenerateTrie would fail.
- if _, err := GenerateTrie(db, scheme, expectedRoot, nil); err != nil {
- t.Fatalf("partial-resume GenerateTrie failed: %v", err)
- }
-
- // All markers cleared on success.
- for i := 0; i < numPartitions; i++ {
- if _, ok := rawdb.ReadGenerateTriePartitionDone(db, byte(i)); ok {
- t.Errorf("partition %d marker not cleared after successful resume", i)
- }
- }
- if scheme == rawdb.PathScheme {
- assertCanonicalNodes(t, db, accounts)
- }
- })
- }
-}
-
// TestHashRanges checks that hashRanges fully and contiguously covers the
// 256-bit hash space, with the last range absorbing the rounding remainder.
func TestHashRanges(t *testing.T) {
@@ -765,22 +673,21 @@ func TestGenerateTrieBatchFlush(t *testing.T) {
tc.build(db)
peak := 0
- var scanned, updated, deleted atomic.Int64
- var pos atomic.Uint64
+ var c genCounters
ranges := hashRanges(numPartitions)
if _, err := generatePartition(context.Background(), nil, peakBatchDB{Database: db, peak: &peak},
- rawdb.HashScheme, 0, ranges[0][0], ranges[0][1], &scanned, &updated, &deleted, &pos); err != nil {
+ rawdb.HashScheme, 0, ranges[0][0], ranges[0][1], &c); err != nil {
t.Fatalf("generatePartition: %v", err)
}
- if scanned.Load() != tc.wantScanned {
- t.Errorf("scanned = %d, want %d (an account was skipped?)", scanned.Load(), tc.wantScanned)
+ if c.accounts.Load() != tc.wantScanned {
+ t.Errorf("scanned = %d, want %d (an account was skipped?)", c.accounts.Load(), tc.wantScanned)
}
- if deleted.Load() != tc.wantDeleted {
- t.Errorf("deleted = %d, want %d", deleted.Load(), tc.wantDeleted)
+ if c.storageDeleted.Load() != tc.wantDeleted {
+ t.Errorf("deleted = %d, want %d", c.storageDeleted.Load(), tc.wantDeleted)
}
- if updated.Load() != 0 {
- t.Errorf("updated = %d, want 0 (a storage slot was dropped across a flush?)", updated.Load())
+ if c.accountUpdated.Load() != 0 {
+ t.Errorf("updated = %d, want 0 (a storage slot was dropped across a flush?)", c.accountUpdated.Load())
}
// The batch must have stayed bounded. Without this site's flush its
// full write set (far larger than IdealBatchSize) buffers into one batch.
diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go
index 3789fae19b..955615bd82 100644
--- a/triedb/pathdb/history_indexer.go
+++ b/triedb/pathdb/history_indexer.go
@@ -766,7 +766,7 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
if err == nil {
version = fmt.Sprintf("%d", m.Version)
}
- log.Info("Cleaned up obsolete history index", "type", typ, "version", version, "want", version)
+ log.Info("Cleaned up obsolete history index", "type", typ, "version", version, "want", fmt.Sprintf("%d", ver))
}
// newHistoryIndexer constructs the history indexer and launches the background
diff --git a/version/version.go b/version/version.go
index 5d402f3009..349220d364 100644
--- a/version/version.go
+++ b/version/version.go
@@ -19,6 +19,6 @@ package version
const (
Major = 1 // Major version component of the current release
Minor = 17 // Minor version component of the current release
- Patch = 4 // Patch version component of the current release
+ Patch = 5 // Patch version component of the current release
Meta = "unstable" // Version metadata to append to the version string
)