Merge branch 'master' into bs/cell-blobpool/sparse-v2

This commit is contained in:
healthykim 2026-06-22 12:50:21 +02:00
commit b4c1a26367
101 changed files with 3392 additions and 1499 deletions

View file

@ -112,7 +112,7 @@ func (arguments Arguments) UnpackIntoMap(v map[string]any, data []byte) error {
// Copy performs the operation go format -> provided struct. // Copy performs the operation go format -> provided struct.
func (arguments Arguments) Copy(v any, values []any) error { func (arguments Arguments) Copy(v any, values []any) error {
// make sure the passed value is arguments pointer // 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) return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
} }
if len(values) == 0 { if len(values) == 0 {

View file

@ -39,7 +39,7 @@ func packElement(t Type, reflectValue reflect.Value) ([]byte, error) {
switch t.T { switch t.T {
case UintTy: case UintTy:
// make sure to not pack a negative value into a uint type. // 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)) val := new(big.Int).Set(reflectValue.Interface().(*big.Int))
if val.Sign() == -1 { if val.Sign() == -1 {
return nil, errInvalidSign return nil, errInvalidSign
@ -86,7 +86,7 @@ func packNum(value reflect.Value) []byte {
return math.U256Bytes(new(big.Int).SetUint64(value.Uint())) return math.U256Bytes(new(big.Int).SetUint64(value.Uint()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return math.U256Bytes(big.NewInt(value.Int())) return math.U256Bytes(big.NewInt(value.Int()))
case reflect.Ptr: case reflect.Pointer:
return math.U256Bytes(new(big.Int).Set(value.Interface().(*big.Int))) return math.U256Bytes(new(big.Int).Set(value.Interface().(*big.Int)))
default: default:
panic("abi: fatal error") panic("abi: fatal error")

View file

@ -53,7 +53,7 @@ func ConvertType(in interface{}, proto interface{}) interface{} {
// indirect recursively dereferences the value until it either gets the value // indirect recursively dereferences the value until it either gets the value
// or finds a big.Int // or finds a big.Int
func indirect(v reflect.Value) reflect.Value { 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 indirect(v.Elem())
} }
return v return v
@ -102,9 +102,9 @@ func mustArrayToByteSlice(value reflect.Value) reflect.Value {
func set(dst, src reflect.Value) error { func set(dst, src reflect.Value) error {
dstType, srcType := dst.Type(), src.Type() dstType, srcType := dst.Type(), src.Type()
switch { 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) 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) return set(dst.Elem(), src)
case srcType.AssignableTo(dstType) && dst.CanSet(): case srcType.AssignableTo(dstType) && dst.CanSet():
dst.Set(src) dst.Set(src)
@ -138,7 +138,7 @@ func setSlice(dst, src reflect.Value) error {
} }
func setArray(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)) return set(dst, indirect(src))
} }
array := reflect.New(dst.Type()).Elem() array := reflect.New(dst.Type()).Elem()

View file

@ -155,7 +155,11 @@ func (s *Suite) sendInvalidTxs(t *utesting.T, txs []*types.Transaction) error {
switch msg := msg.(type) { switch msg := msg.(type) {
case *eth.TransactionsPacket: 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 { if _, ok := invalids[tx.Hash()]; ok {
return fmt.Errorf("received bad tx: %s", tx.Hash()) return fmt.Errorf("received bad tx: %s", tx.Hash())
} }

View file

@ -191,6 +191,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
Difficulty: pre.Env.Difficulty, Difficulty: pre.Env.Difficulty,
GasLimit: pre.Env.GasLimit, GasLimit: pre.Env.GasLimit,
GetHash: getHash, GetHash: getHash,
CostPerStateByte: params.CostPerStateByte,
} }
if pre.Env.SlotNumber != nil { if pre.Env.SlotNumber != nil {
vmContext.SlotNum = *pre.Env.SlotNumber vmContext.SlotNum = *pre.Env.SlotNumber

View file

@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error {
} }
// Check intrinsic gas // Check intrinsic gas
rules := chainConfig.Rules(common.Big0, true, 0) 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 { if err != nil {
r.Error = err r.Error = err
results = append(results, r) results = append(results, r)

View file

@ -96,7 +96,7 @@ func testConsoleLogging(t *testing.T, format string, tStart, tEnd int) {
} }
} }
if len(haveLines) != len(wantLines) { 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))
} }
} }

View file

@ -58,6 +58,7 @@ import (
"github.com/ethereum/go-ethereum/graphql" "github.com/ethereum/go-ethereum/graphql"
"github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/flags" "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/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/metrics/exp" "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/hashdb"
"github.com/ethereum/go-ethereum/triedb/pathdb" "github.com/ethereum/go-ethereum/triedb/pathdb"
pcsclite "github.com/gballet/go-libpcsclite" pcsclite "github.com/gballet/go-libpcsclite"
gopsutil "github.com/shirou/gopsutil/mem"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -1726,16 +1726,18 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
setMiner(ctx, &cfg.Miner) setMiner(ctx, &cfg.Miner)
setRequiredBlocks(ctx, cfg) setRequiredBlocks(ctx, cfg)
// Cap the cache allowance and tune the garbage collector // Cap the cache allowance and tune the garbage collector against
mem, err := gopsutil.VirtualMemory() // the effective memory limit (cgroup-imposed when running in a
if err == nil { // container, total system memory otherwise).
if 32<<(^uintptr(0)>>63) == 32 && mem.Total > 2*1024*1024*1024 { total, source := memlimit.Limit()
log.Warn("Lowering memory allowance on 32bit arch", "available", mem.Total/1024/1024, "addressable", 2*1024) if total > 0 {
mem.Total = 2 * 1024 * 1024 * 1024 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 { 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)) 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 cfg.SyncMode = ethconfig.FullSync // dev sync target forces full sync
} else if ctx.IsSet(SyncModeFlag.Name) { } else if ctx.IsSet(SyncModeFlag.Name) {
value := ctx.String(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) Fatalf("--%v: %v", SyncModeFlag.Name, err)
} }
} }
if ctx.IsSet(ChainHistoryFlag.Name) { if ctx.IsSet(ChainHistoryFlag.Name) {
value := ctx.String(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) Fatalf("--%s: %v", ChainHistoryFlag.Name, err)
} }
} }

View file

@ -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 --- // --- tx builders ---
func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction { 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) env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) { 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) sender := assertPresent(t, b, env.from)
@ -260,7 +268,7 @@ func TestBALSystemAddressIncludedWhenTouched(t *testing.T) {
env := newBALTestEnv(nil) env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, sys)
@ -283,7 +291,7 @@ func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, identity)
@ -315,7 +323,7 @@ func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) {
env := newBALTestEnv(nil) env := newBALTestEnv(nil)
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, identity)
@ -334,7 +342,7 @@ func TestBALBalanceProbeOnNonExistent(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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)) 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}}) env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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)) 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}}) env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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)) 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}}) env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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)) assertEmpty(t, assertPresent(t, b, probe))
@ -447,7 +455,7 @@ func TestBALCallTargetWithEmptyChangeSet(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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)) assertEmpty(t, assertPresent(t, b, target))
@ -465,7 +473,7 @@ func TestBALCallCodeTargetIncluded(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) assertPresent(t, b, caller)
@ -483,7 +491,7 @@ func TestBALDelegateCallTargetIncluded(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) assertPresent(t, b, caller)
@ -501,7 +509,7 @@ func TestBALStaticCallTargetIncluded(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) assertPresent(t, b, caller)
@ -519,7 +527,7 @@ func TestBALRevertedTxStillIncluded(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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)) 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}}) env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) 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}}) env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, contract)
@ -578,7 +586,7 @@ func TestBALStorageSloadOnly(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, contract)
@ -604,7 +612,7 @@ func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, contract)
@ -632,7 +640,7 @@ func TestBALNoOpSSTOREDemotesToRead(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, contract)
@ -660,7 +668,7 @@ func TestBALStorageWriteZeroIsAWrite(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) 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} init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) { 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 created := receipts[0].ContractAddress
@ -711,7 +719,7 @@ func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) {
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, receipts := env.run(t, func(g *BlockGen) { 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 created := receipts[0].ContractAddress
@ -732,7 +740,7 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
b, receipts := env.run(t, func(g *BlockGen) { 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 created := receipts[0].ContractAddress
@ -743,11 +751,13 @@ func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) {
// the deployed address in the BAL with an empty change set. // the deployed address in the BAL with an empty change set.
func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) { func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) {
env := newBALTestEnv(nil) 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} init := []byte{0x5b, 0x60, 0x00, 0x56}
b, receipts := env.run(t, func(g *BlockGen) { 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 created := receipts[0].ContractAddress
@ -771,7 +781,7 @@ func TestBALCreateAddressCollisionStillIncluded(t *testing.T) {
// Init code doesn't matter — execution never starts. // Init code doesn't matter — execution never starts.
init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3}
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, collide)
@ -799,7 +809,7 @@ func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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. // 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}}) env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}})
b, _ := env.run(t, func(g *BlockGen) { 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, // 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) init = append(init, 0xff)
b, receipts := env.run(t, func(g *BlockGen) { 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 created := receipts[0].ContractAddress
@ -896,7 +906,7 @@ func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) {
init = append(init, 0xff) init = append(init, 0xff)
b, _ := env.run(t, func(g *BlockGen) { 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) ben := assertPresent(t, b, beneficiary)
@ -921,7 +931,7 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) {
}) })
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, suicidal)
@ -966,7 +976,7 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) {
env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}}) env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}})
b, _ := env.run(t, func(g *BlockGen) { 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) aa := assertPresent(t, b, bouncer)
@ -1072,7 +1082,7 @@ func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) {
Nonce: 0, Nonce: 0,
To: env.from, To: env.from,
Value: new(uint256.Int), Value: new(uint256.Int),
Gas: 200_000, Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int), GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth}, AuthList: []types.SetCodeAuthorization{auth},
@ -1112,7 +1122,7 @@ func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) {
Nonce: 0, Nonce: 0,
To: env.from, // tx.to is an EOA with no code: delegate is never called To: env.from, // tx.to is an EOA with no code: delegate is never called
Value: new(uint256.Int), Value: new(uint256.Int),
Gas: 200_000, Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int), GasTipCap: new(uint256.Int),
AuthList: []types.SetCodeAuthorization{auth}, AuthList: []types.SetCodeAuthorization{auth},
@ -1131,7 +1141,7 @@ func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address,
Nonce: nonce, Nonce: nonce,
To: to, To: to,
Value: new(uint256.Int), Value: new(uint256.Int),
Gas: 400_000, Gas: 1_000_000,
GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
GasTipCap: new(uint256.Int), GasTipCap: new(uint256.Int),
AuthList: auths, AuthList: auths,

View file

@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
data := make([]byte, nbytes) data := make([]byte, nbytes)
return func(i int, gen *BlockGen) { return func(i int, gen *BlockGen) {
toaddr := common.Address{} 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() signer := gen.Signer()
gasPrice := big.NewInt(0) gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil { if gen.header.BaseFee != nil {

View file

@ -64,12 +64,12 @@ var (
func TestProcessUBT(t *testing.T) { func TestProcessUBT(t *testing.T) {
var ( var (
code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) 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 // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness
// will not contain that copied data. // will not contain that copied data.
// Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985
codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) 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) signer = types.LatestSigner(testUBTChainConfig)
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain
@ -201,7 +201,7 @@ func TestProcessParentBlockHash(t *testing.T) {
if isUBT { if isUBT {
chainConfig = testUBTChainConfig 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{}) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{})
ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList())
} }

View file

@ -1194,10 +1194,13 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash, isSnapV2 bool) error {
return fmt.Errorf("non existent state [%x..]", root[:4]) return fmt.Errorf("non existent state [%x..]", root[:4])
} }
// The legacy snapshot tree needs to be wiped and rebuilt from the trie // The legacy snapshot tree (hash scheme only) was persistently disabled
// after a snap/1 sync. // before the sync, re-enables it explicitly.
if !isSnapV2 && bc.snaps != nil { //
bc.snaps.Rebuild(root) // 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. // 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.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
stats.CrossValidation = xvtime // The time spent on stateless cross 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. // Write the block to the chain and get the status.
var status WriteStatus var status WriteStatus
if config.WriteState { if config.WriteState {

View file

@ -29,6 +29,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
@ -721,3 +722,40 @@ func TestRecoverSnapshotFromWipingCrash(t *testing.T) {
test.teardown() 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()
}
}

View file

@ -80,6 +80,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
GasLimit: header.GasLimit, GasLimit: header.GasLimit,
Random: random, Random: random,
SlotNum: slotNum, SlotNum: slotNum,
CostPerStateByte: params.CostPerStateByte,
} }
} }

View file

@ -27,6 +27,10 @@ type GasPool struct {
remaining uint64 remaining uint64
initial uint64 initial uint64
cumulativeUsed uint64 cumulativeUsed uint64
// After 8037 Block gas used is max(cumulativeRegular, cumulativeState).
cumulativeRegular uint64
cumulativeState uint64
} }
// NewGasPool initializes the gasPool with the given amount. // 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. // available and returns an error otherwise.
func (gp *GasPool) SubGas(amount uint64) error { func (gp *GasPool) CheckGasLegacy(amount uint64) error {
if gp.remaining < amount { if gp.remaining < amount {
return ErrGasLimitReached return ErrGasLimitReached
} }
@ -47,41 +51,73 @@ func (gp *GasPool) SubGas(amount uint64) error {
return nil 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. // 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 { if gp.remaining > math.MaxUint64-returned {
return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, 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) // returned = purchased - remaining (refund included)
//
// - Post-Amsterdam:
// returned = purchased - gasUsed (refund excluded)
gp.remaining += returned gp.remaining += returned
// gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost) // gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost)
// regardless of Amsterdam is activated or not.
gp.cumulativeUsed += gasUsed gp.cumulativeUsed += gasUsed
return nil 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. // Gas returns the amount of gas remaining in the pool.
func (gp *GasPool) Gas() uint64 { func (gp *GasPool) Gas() uint64 {
return gp.remaining 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 { func (gp *GasPool) CumulativeUsed() uint64 {
return gp.cumulativeUsed return gp.cumulativeUsed
} }
// Used returns the amount of consumed gas. // Used returns the amount of consumed gas.
func (gp *GasPool) Used() uint64 { 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 { 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 return gp.initial - gp.remaining
} }
@ -92,6 +128,8 @@ func (gp *GasPool) Snapshot() *GasPool {
initial: gp.initial, initial: gp.initial,
remaining: gp.remaining, remaining: gp.remaining,
cumulativeUsed: gp.cumulativeUsed, cumulativeUsed: gp.cumulativeUsed,
cumulativeRegular: gp.cumulativeRegular,
cumulativeState: gp.cumulativeState,
} }
} }
@ -100,6 +138,8 @@ func (gp *GasPool) Set(other *GasPool) {
gp.initial = other.initial gp.initial = other.initial
gp.remaining = other.remaining gp.remaining = other.remaining
gp.cumulativeUsed = other.cumulativeUsed gp.cumulativeUsed = other.cumulativeUsed
gp.cumulativeRegular = other.cumulativeRegular
gp.cumulativeState = other.cumulativeState
} }
func (gp *GasPool) String() string { func (gp *GasPool) String() string {

View file

@ -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. // DeleteSnapshotSyncStatus removes the serialized sync status from the database.
func DeleteSnapshotSyncStatus(db ethdb.KeyValueWriter) { func DeleteSnapshotSyncStatus(db ethdb.KeyValueWriter) {
if err := db.Delete(snapshotSyncStatusKey); err != nil { if err := db.Delete(snapshotSyncStatusKey); err != nil {

View file

@ -563,8 +563,6 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
} }
// Metadata keys // 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) }): case slices.ContainsFunc(knownMetadataKeys, func(x []byte) bool { return bytes.Equal(x, key) }):
metadata.add(size) metadata.add(size)

View file

@ -104,10 +104,6 @@ var (
// snapSyncStatusFlagKey flags that status of snap sync. // snapSyncStatusFlagKey flags that status of snap sync.
snapSyncStatusFlagKey = []byte("SnapSyncStatus") 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). // 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 headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td (deprecated) 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 { func transitionStateKey(hash common.Hash) []byte {
return append(VerkleTransitionStatePrefix, hash.Bytes()...) return append(VerkleTransitionStatePrefix, hash.Bytes()...)
} }
// generateTriePartitionDoneKey = generateTriePartitionDonePrefix + partition (single byte).
func generateTriePartitionDoneKey(partition byte) []byte {
return append(generateTriePartitionDonePrefix, partition)
}

View file

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "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 { if err != nil {
log.Warn("Failed to load snapshot", "err", err) log.Warn("Failed to load snapshot", "err", err)
if !config.NoBuild { if !config.NoBuild {
snap.Rebuild(root) snap.Rebuild(root, true)
return snap, nil return snap, nil
} }
return nil, err // Bail out the error, don't rebuild automatically. 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 return base, nil
} }
// Rebuild wipes all available snapshot data from the persistent database and // Rebuild discards all caches and diff layers and re-enables the snapshot
// discard all caches and diff layers. Afterwards, it starts a new snapshot // feature. With generate set, it starts a new snapshot generator with the
// generator with the given root hash. // given root hash, wiping and regenerating the persistent flat state in the
func (t *Tree) Rebuild(root common.Hash) { // 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() t.lock.Lock()
defer t.lock.Unlock() defer t.lock.Unlock()
@ -715,6 +718,26 @@ func (t *Tree) Rebuild(root common.Hash) {
panic(fmt.Sprintf("unknown layer type: %T", layer)) 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 // 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. // generator will run a wiper first if there's not one running right now.
log.Info("Rebuilding state snapshot") log.Info("Rebuilding state snapshot")

View file

@ -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) 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 // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root
// contract. This method is exported to be used in tests. // contract. This method is exported to be used in tests.
func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { 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() defer tracer.OnSystemCallEnd()
} }
} }
gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: gasLimit,
GasPrice: uint256.NewInt(0), GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0),
GasTipCap: 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.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) 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 { if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents) evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
} }
@ -309,9 +325,10 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *
defer tracer.OnSystemCallEnd() defer tracer.OnSystemCallEnd()
} }
} }
gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: gasLimit,
GasPrice: uint256.NewInt(0), GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0),
GasTipCap: 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.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.SetTxContext(common.Hash{}, 0, 0)
evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) 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 { if err != nil {
panic(err) panic(err)
} }
@ -351,9 +368,10 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E
defer tracer.OnSystemCallEnd() defer tracer.OnSystemCallEnd()
} }
} }
gasLimit, gasBudget := systemCallGasBudget(evm)
msg := &Message{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: gasLimit,
GasPrice: uint256.NewInt(0), GasPrice: uint256.NewInt(0),
GasFeeCap: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0),
GasTipCap: 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.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil)
evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex)
evm.StateDB.AddAddressToAccessList(addr) 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 { if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents) evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
} }

View file

@ -68,13 +68,27 @@ func (result *ExecutionResult) Revert() []byte {
} }
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data. // 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 // Set the starting gas for the raw transaction
var gas uint64 var gas vm.GasCosts
if isContractCreation && isHomestead { if isContractCreation && rules.IsHomestead {
gas = params.TxGasContractCreation if rules.IsAmsterdam {
gas.RegularGas = params.TxGas + params.CreateGasAmsterdam
gas.StateGas = params.AccountCreationSize * costPerStateByte
} else { } else {
gas = params.TxGas gas.RegularGas = params.TxGasContractCreation
}
} else {
gas.RegularGas = params.TxGas
}
// Add gas for authorizations
if authList != nil {
if rules.IsAmsterdam {
gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas
gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte
} else {
gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas
}
} }
dataLen := uint64(len(data)) dataLen := uint64(len(data))
// Bump the required gas by the amount of transactional 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 // Make sure we don't exceed uint64 for all data combinations
nonZeroGas := params.TxDataNonZeroGasFrontier nonZeroGas := params.TxDataNonZeroGasFrontier
if isEIP2028 { if rules.IsIstanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028 nonZeroGas = params.TxDataNonZeroGasEIP2028
} }
if (math.MaxUint64-gas)/nonZeroGas < nz { if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz {
return vm.GasCosts{}, ErrGasUintOverflow 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 return vm.GasCosts{}, ErrGasUintOverflow
} }
gas += z * params.TxDataZeroGas gas.RegularGas += z * params.TxDataZeroGas
if isContractCreation && isEIP3860 { if isContractCreation && rules.IsShanghai {
lenWords := toWordSize(dataLen) lenWords := toWordSize(dataLen)
if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords {
return vm.GasCosts{}, ErrGasUintOverflow return vm.GasCosts{}, ErrGasUintOverflow
} }
gas += lenWords * params.InitCodeWordGas gas.RegularGas += lenWords * params.InitCodeWordGas
} }
} }
if accessList != nil { if accessList != nil {
addresses := uint64(len(accessList)) addresses := uint64(len(accessList))
storageKeys := uint64(accessList.StorageKeys()) storageKeys := uint64(accessList.StorageKeys())
if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses {
return vm.GasCosts{}, ErrGasUintOverflow return vm.GasCosts{}, ErrGasUintOverflow
} }
gas += addresses * params.TxAccessListAddressGas gas.RegularGas += addresses * params.TxAccessListAddressGas
if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow 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. // EIP-7981: access list data is charged in addition to the base charge.
if isAmsterdam { if rules.IsAmsterdam {
const ( const (
addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte
storageKeyCost = common.HashLength * 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 return vm.GasCosts{}, ErrGasUintOverflow
} }
gas += addresses * addressCost gas.RegularGas += addresses * addressCost
if (math.MaxUint64-gas)/storageKeyCost < storageKeys { if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow return vm.GasCosts{}, ErrGasUintOverflow
} }
gas += storageKeys * storageKeyCost gas.RegularGas += storageKeys * storageKeyCost
} }
} }
if authList != nil { return gas, nil
gas += uint64(len(authList)) * params.CallNewAccountGas
}
return vm.GasCosts{RegularGas: gas}, nil
} }
// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623).
@ -340,7 +351,6 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err
type stateTransition struct { type stateTransition struct {
gp *GasPool gp *GasPool
msg *Message msg *Message
initialBudget vm.GasBudget
gasRemaining vm.GasBudget gasRemaining vm.GasBudget
state vm.StateDB state vm.StateDB
evm *vm.EVM evm *vm.EVM
@ -364,6 +374,24 @@ func (st *stateTransition) to() common.Address {
return *st.msg.To 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 { func (st *stateTransition) buyGas() error {
mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) mgval := new(uint256.Int).SetUint64(st.msg.GasLimit)
_, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) _, 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 { 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) 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 return err
} }
if st.evm.Config.Tracer.HasGasHook() { // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit
empty := vm.GasBudget{} limit := st.msg.GasLimit
initial := vm.NewGasBudget(st.msg.GasLimit) if isAmsterdam {
st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance) limit = min(st.msg.GasLimit, params.MaxTxGas)
} }
st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) st.gasRemaining = vm.NewGasBudget(limit, st.msg.GasLimit-limit)
st.initialBudget = st.gasRemaining.Copy()
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) st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy)
return nil 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 { func (st *stateTransition) preCheck() error {
// Only check transactions that are not fake // Only check transactions that are not fake
msg := st.msg msg := st.msg
@ -449,11 +508,13 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce) msg.From.Hex(), stNonce)
} }
} }
isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) var (
isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) 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 { if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap. // 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) return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
} }
// Make sure the sender is an EOA // Make sure the sender is an EOA
@ -527,40 +588,33 @@ func (st *stateTransition) preCheck() error {
return st.buyGas() return st.buyGas()
} }
// execute will transition the state by applying the current message and // execute transitions the state by applying the current message and
// returning the evm execution result with following fields. // returns the EVM execution result with the following fields:
// //
// - used gas: total gas used (including gas being refunded) // - used gas: total gas used, including gas refunded
// - returndata: the returned data from evm // - peak used gas: maximum gas used before applying refunds
// - concrete execution error: various EVM errors which abort the execution, e.g. // - returndata: data returned by the EVM
// ErrOutOfGas, ErrExecutionReverted // - execution error: EVM-level errors that abort execution, such as
// ErrOutOfGas or ErrExecutionReverted
// //
// However if any consensus issue encountered, return the error directly with // If a consensus error is encountered, it is returned directly with a
// nil evm execution result. // nil EVM execution result.
func (st *stateTransition) execute() (*ExecutionResult, error) { func (st *stateTransition) execute() (*ExecutionResult, error) {
// First check this message satisfies all consensus rules before // Validate the message and pre-pay gas.
// 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
if err := st.preCheck(); err != nil { if err := st.preCheck(); err != nil {
return nil, err 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 ( var (
msg = st.msg msg = st.msg
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
contractCreation = msg.To == nil contractCreation = msg.To == nil
floorDataGas uint64 floorDataGas uint64
) )
// Check clauses 4-5, subtract intrinsic gas if everything is correct cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte)
cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -571,15 +625,24 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if st.evm.Config.Tracer.HasGasHook() { if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) 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 { if rules.IsPrague {
floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Make sure the transaction has sufficient gas allowance to
// pay the floor cost.
if msg.GasLimit < floorDataGas { if msg.GasLimit < floorDataGas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, 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 { 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 value := msg.Value
if value == nil { if value == nil {
value = new(uint256.Int) 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()) 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: // Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin) // - prepare accessList(post-berlin)
// - reset transient storage(EIP-1153) // - reset transient storage(EIP-1153)
// - enable block-level accessList construction (EIP-7928) // - enable block-level accessList construction (EIP-7928)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
// Execute the top-most frame
var ( var (
ret []byte ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err 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 { 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 { } else {
// Increment the nonce for the next transaction. // Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
// Apply EIP-7702 authorizations. // Apply EIP-7702 authorizations.
if msg.SetCodeAuthorizations != nil { st.applyAuthorizations(rules, msg.SetCodeAuthorizations)
for _, auth := range msg.SetCodeAuthorizations {
// Note errors are ignored, we simply skip invalid authorizations here.
st.applyAuthorization(&auth)
}
}
// Perform convenience warming of sender's delegation target. Although the // Perform convenience warming of sender's delegation target. Although the
// sender is already warmed in Prepare(..), it's possible a delegation to // 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 { if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok {
st.state.AddAddressToAccessList(addr) st.state.AddAddressToAccessList(addr)
} }
// Execute the transaction's call. // 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 // Settle down the gas usage and refund the ETH back if any remaining
// gas allowance required to complete execution. gasUsed, peakUsed, err := st.settleGas(rules, floorDataGas)
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())
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Pay the effective transaction fee to the specific coinbase
effectiveTip := msg.GasPrice effectiveTip := msg.GasPrice
if rules.IsLondon { if rules.IsLondon {
baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee) 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) effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee)
} }
if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
// Skip fee payment when NoBaseFee is set and the fee fields // Skip fee payment when NoBaseFee is set and the fee fields
// are 0. This avoids a negative effectiveTip being applied to // are 0. This avoids a negative effectiveTip being applied to
// the coinbase when simulating calls. // the coinbase when simulating calls.
} else { } else {
fee := new(uint256.Int).SetUint64(st.gasUsed()) fee := new(uint256.Int).SetUint64(gasUsed)
fee.Mul(fee, effectiveTip) fee.Mul(fee, effectiveTip)
st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) 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) st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64)
} }
} }
// EIP-7708: Emit the ETH-burn logs
if rules.IsAmsterdam { if rules.IsAmsterdam {
for _, log := range st.evm.StateDB.LogsForBurnAccounts() { for _, log := range st.evm.StateDB.LogsForBurnAccounts() {
st.evm.StateDB.AddLog(log) st.evm.StateDB.AddLog(log)
} }
} }
return &ExecutionResult{ return &ExecutionResult{
UsedGas: st.gasUsed(), UsedGas: gasUsed,
MaxUsedGas: peakGasUsed, MaxUsedGas: peakUsed,
Err: vmerr, Err: vmerr,
ReturnData: ret, ReturnData: ret,
}, nil }, 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. // validateAuthorization validates an EIP-7702 authorization against the state.
func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) { func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
// Verify chain ID is null or equal to current chain ID. // 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 return authority, nil
} }
// applyAuthorization applies an EIP-7702 code delegation to the state. // applyAuthorization applies an EIP-7702 code delegation to the state and,
func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { // 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) authority, err := st.validateAuthorization(auth)
if err != nil { if err != nil {
if rules.IsAmsterdam {
st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte)
}
return err return err
} }
prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority))
// If the account already exists in state, refund the new account cost if !rules.IsAmsterdam {
// charged in the intrinsic calculation.
if st.state.Exist(authority) { if st.state.Exist(authority) {
st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) 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. // Update nonce and account code.
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
if auth.Address == (common.Address{}) {
// Delegation to zero address means clear. // Delegation to zero address means clear.
if auth.Address == (common.Address{}) {
if curDelegated {
st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
}
return nil return nil
} }
// Install delegation to auth.Address if the delegation changed
// Otherwise install delegation to auth.Address. if !curDelegated || auth.Address != prevDelegation {
st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
}
return nil return nil
} }
// calcRefund computes refund counter, capped to a refund quotient. // applyAuthorizations applies an EIP-7702 code delegation to the state.
func (st *stateTransition) calcRefund() vm.GasBudget { func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) {
var refund uint64 preDelegated := make(map[common.Address]bool)
if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { for _, auth := range auths {
// Before EIP-3529: refunds were capped to gasUsed / 2 st.applyAuthorization(rules, &auth, preDelegated)
refund = st.gasUsed() / params.RefundQuotient
} else {
// After EIP-3529: refunds are capped to gasUsed / 5
refund = st.gasUsed() / params.RefundQuotientEIP3529
} }
}
// 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() { if refund > st.state.GetRefund() {
refund = st.state.GetRefund() refund = st.state.GetRefund()
} }
if refund > 0 && st.evm.Config.Tracer.HasGasHook() { return refund
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)
} }
// blobGasUsed returns the amount of blob gas used by the message. // blobGasUsed returns the amount of blob gas used by the message.

View file

@ -155,50 +155,50 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028 bool isEIP2028 bool
isEIP3860 bool isEIP3860 bool
isAmsterdam bool isAmsterdam bool
want uint64 want vm.GasCosts
}{ }{
{ {
name: "frontier/empty-call", name: "frontier/empty-call",
want: params.TxGas, want: vm.GasCosts{RegularGas: params.TxGas},
}, },
{ {
name: "frontier/contract-creation-pre-homestead", name: "frontier/contract-creation-pre-homestead",
creation: true, creation: true,
isHomestead: false, isHomestead: false,
// pre-homestead, contract creation still uses TxGas // pre-homestead, contract creation still uses TxGas
want: params.TxGas, want: vm.GasCosts{RegularGas: params.TxGas},
}, },
{ {
name: "homestead/contract-creation", name: "homestead/contract-creation",
creation: true, creation: true,
isHomestead: true, isHomestead: true,
want: params.TxGasContractCreation, want: vm.GasCosts{RegularGas: params.TxGasContractCreation},
}, },
{ {
name: "frontier/non-zero-data", name: "frontier/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100), data: bytes.Repeat([]byte{0xff}, 100),
// 100 nz bytes * 68 (frontier) // 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", name: "istanbul/non-zero-data",
data: bytes.Repeat([]byte{0xff}, 100), data: bytes.Repeat([]byte{0xff}, 100),
isEIP2028: true, isEIP2028: true,
// 100 nz bytes * 16 (post-EIP2028) // 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", name: "istanbul/zero-data",
data: bytes.Repeat([]byte{0x00}, 100), data: bytes.Repeat([]byte{0x00}, 100),
isEIP2028: true, isEIP2028: true,
// 100 zero bytes * 4 // 100 zero bytes * 4
want: params.TxGas + 100*params.TxDataZeroGas, want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas},
}, },
{ {
name: "istanbul/mixed-data", name: "istanbul/mixed-data",
data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...),
isEIP2028: true, 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", name: "shanghai/init-code-word-gas",
@ -208,7 +208,7 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isEIP3860: true, isEIP3860: true,
// TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 // 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", name: "shanghai/init-code-non-multiple-of-32",
@ -217,7 +217,7 @@ func TestIntrinsicGas(t *testing.T) {
isHomestead: true, isHomestead: true,
isEIP2028: true, isEIP2028: true,
isEIP3860: 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", name: "berlin/access-list",
@ -227,7 +227,7 @@ func TestIntrinsicGas(t *testing.T) {
}, },
isEIP2028: true, isEIP2028: true,
// 2 addrs * 2400 + 3 keys * 1900 // 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", name: "amsterdam/access-list-extra-cost",
@ -238,9 +238,9 @@ func TestIntrinsicGas(t *testing.T) {
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
// base access-list charge + EIP-7981 extra // base access-list charge + EIP-7981 extra
want: params.TxGas + want: vm.GasCosts{RegularGas: params.TxGas +
2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas +
2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost},
}, },
{ {
name: "prague/auth-list", name: "prague/auth-list",
@ -250,8 +250,54 @@ func TestIntrinsicGas(t *testing.T) {
{Address: addr1}, {Address: addr1},
}, },
isEIP2028: true, isEIP2028: true,
// 3 auths * 25000 // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple)
want: params.TxGas + 3*params.CallNewAccountGas, 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", name: "amsterdam/combined",
@ -264,23 +310,34 @@ func TestIntrinsicGas(t *testing.T) {
}, },
isEIP2028: true, isEIP2028: true,
isAmsterdam: true, isAmsterdam: true,
want: params.TxGas + // 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 + 100*params.TxDataNonZeroGasEIP2028 +
1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
1*params.CallNewAccountGas, 1*params.TxAuthTupleRegularGas,
StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte,
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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, 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 { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
want := vm.GasCosts{RegularGas: tt.want} if got != tt.want {
if got != want { t.Fatalf("gas mismatch: got %+v, want %+v", got, tt.want)
t.Fatalf("gas mismatch: got %+v, want %+v", got, want)
} }
}) })
} }

View file

@ -28,21 +28,22 @@ func _() {
_ = x[GasChangeWitnessCodeChunk-17] _ = x[GasChangeWitnessCodeChunk-17]
_ = x[GasChangeWitnessContractCollisionCheck-18] _ = x[GasChangeWitnessContractCollisionCheck-18]
_ = x[GasChangeTxDataFloor-19] _ = x[GasChangeTxDataFloor-19]
_ = x[GasChangeAccountCreation-20]
_ = x[GasChangeIgnored-255] _ = x[GasChangeIgnored-255]
} }
const ( const (
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloor" _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorAccountCreation"
_GasChangeReason_name_1 = "Ignored" _GasChangeReason_name_1 = "Ignored"
) )
var ( 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 { func (i GasChangeReason) String() string {
switch { switch {
case i <= 19: case i <= 20:
return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]]
case i == 255: case i == 255:
return _GasChangeReason_name_1 return _GasChangeReason_name_1

View file

@ -472,6 +472,10 @@ const (
// transaction data. This change will always be a negative change. // transaction data. This change will always be a negative change.
GasChangeTxDataFloor GasChangeReason = 19 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 // 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. // it will be "manually" tracked by a direct emit of the gas change event.
GasChangeIgnored GasChangeReason = 0xFF GasChangeIgnored GasChangeReason = 0xFF

View file

@ -279,10 +279,9 @@ func encodeForNetwork(storedRLP []byte, version uint) ([]byte, error) {
// 3. Find the version of sidecar. // 3. Find the version of sidecar.
sidecarVersion, _, err := rlp.SplitUint64(sidecarElems[0]) 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) return nil, fmt.Errorf("invalid version: %w", err)
} }
sidecarVersionByte := byte(sidecarVersion)
// 4. Extract sidecar elements. // 4. Extract sidecar elements.
commitmentsRLP := sidecarElems[2] commitmentsRLP := sidecarElems[2]
@ -315,12 +314,7 @@ func encodeForNetwork(storedRLP []byte, version uint) ([]byte, error) {
} }
// 6. Reconstruct into the network format. // 6. Reconstruct into the network format.
var outer [][]byte outer := [][]byte{txRLP, sidecarElems[0], blobsField, commitmentsRLP, proofsRLP}
if sidecarVersionByte == types.BlobSidecarVersion0 {
outer = [][]byte{txRLP, blobsField, commitmentsRLP, proofsRLP}
} else {
outer = [][]byte{txRLP, sidecarElems[0], blobsField, commitmentsRLP, proofsRLP}
}
body, err := rlp.MergeListValues(outer) body, err := rlp.MergeListValues(outer)
if err != nil { if err != nil {
return nil, err return nil, err
@ -634,7 +628,7 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
p.state = state p.state = state
// Create new slotter for pre-Osaka blob configuration. // 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 // See if we need to migrate the queue blob store after fusaka
slotter, err = tryMigrate(p.chain.Config(), slotter, queuedir) slotter, err = tryMigrate(p.chain.Config(), slotter, queuedir)

View file

@ -53,7 +53,6 @@ import (
var ( var (
testBlobs []*kzg4844.Blob testBlobs []*kzg4844.Blob
testBlobCommits []kzg4844.Commitment testBlobCommits []kzg4844.Commitment
testBlobProofs []kzg4844.Proof
testBlobCellProofs [][]kzg4844.Proof testBlobCellProofs [][]kzg4844.Proof
testBlobVHashes [][32]byte testBlobVHashes [][32]byte
testBlobIndices = make(map[[32]byte]int) testBlobIndices = make(map[[32]byte]int)
@ -69,9 +68,6 @@ func init() {
testBlobCommit, _ := kzg4844.BlobToCommitment(testBlob) testBlobCommit, _ := kzg4844.BlobToCommitment(testBlob)
testBlobCommits = append(testBlobCommits, testBlobCommit) testBlobCommits = append(testBlobCommits, testBlobCommit)
testBlobProof, _ := kzg4844.ComputeBlobProof(testBlob, testBlobCommit)
testBlobProofs = append(testBlobProofs, testBlobProof)
testBlobCellProof, _ := kzg4844.ComputeCellProofs(testBlob) testBlobCellProof, _ := kzg4844.ComputeCellProofs(testBlob)
testBlobCellProofs = append(testBlobCellProofs, testBlobCellProof) 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 // makeMultiBlobTx is a utility method to construct a random blob tx with
// certain number of blobs in its sidecar. // 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 ( var (
blobs []kzg4844.Blob blobs []kzg4844.Blob
blobHashes []common.Hash blobHashes []common.Hash
@ -254,12 +250,8 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
for i := 0; i < blobCount; i++ { for i := 0; i < blobCount; i++ {
blobs = append(blobs, *testBlobs[blobOffset+i]) blobs = append(blobs, *testBlobs[blobOffset+i])
commitments = append(commitments, testBlobCommits[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]) cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i])
proofs = append(proofs, cellProofs...) proofs = append(proofs, cellProofs...)
}
blobHashes = append(blobHashes, testBlobVHashes[blobOffset+i]) blobHashes = append(blobHashes, testBlobVHashes[blobOffset+i])
} }
blobtx := &types.BlobTx{ blobtx := &types.BlobTx{
@ -271,7 +263,7 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
BlobFeeCap: uint256.NewInt(blobFeeCap), BlobFeeCap: uint256.NewInt(blobFeeCap),
BlobHashes: blobHashes, BlobHashes: blobHashes,
Value: uint256.NewInt(100), 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) 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), BlobFeeCap: uint256.NewInt(blobFeeCap),
BlobHashes: []common.Hash{testBlobVHashes[blobIdx]}, BlobHashes: []common.Hash{testBlobVHashes[blobIdx]},
Value: uint256.NewInt(100), 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...) hashes = append(hashes, tx.vhashes...)
} }
} }
blobs1, _, proofs1, err := pool.getBlobs(hashes, types.BlobSidecarVersion0) blobs, _, proofs, err := pool.getBlobs(hashes, types.BlobSidecarVersion1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
blobs2, _, proofs2, err := pool.getBlobs(hashes, types.BlobSidecarVersion1) if len(blobs) != len(hashes) || len(proofs) != len(hashes) {
if err != nil { t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want blobs %d, want proofs: %d", len(blobs), len(proofs), len(hashes), len(hashes))
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))
return return
} }
for i, hash := range hashes { 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 // Item retrieved, make sure it matches the expectation
index := testBlobIndices[hash] index := testBlobIndices[hash]
if blobs1[i] != nil && (*blobs1[i] != *testBlobs[index] || proofs1[i][0] != testBlobProofs[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
}
if blobs2[i] != nil && (*blobs2[i] != *testBlobs[index] || !slices.Equal(proofs2[i], testBlobCellProofs[index])) {
t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash) t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
continue continue
} }
@ -513,7 +487,7 @@ func TestOpenDrops(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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 // Insert a malformed transaction to verify that decoding errors (or format
// changes) are handled gracefully (case 1) // changes) are handled gracefully (case 1)
@ -841,7 +815,7 @@ func TestOpenIndex(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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 // Insert a sequence of transactions with varying price points to check that
// the cumulative minimum will be maintained. // the cumulative minimum will be maintained.
@ -929,7 +903,7 @@ func TestOpenHeap(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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 // Insert a few transactions from a few accounts. To remove randomness from
// the heap initialization, use a deterministic account/tx/priority ordering. // the heap initialization, use a deterministic account/tx/priority ordering.
@ -1107,7 +1081,7 @@ func TestChangingSlotterSize(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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. // Create transactions from a few accounts.
var ( var (
@ -1119,9 +1093,9 @@ func TestChangingSlotterSize(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey) addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey) addr3 = crypto.PubkeyToAddress(key3.PublicKey)
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0) tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3)
blob1 = encodeForPool(tx1) blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2) blob2 = encodeForPool(tx2)
@ -1222,9 +1196,9 @@ func TestBillyMigration(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey) addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey) addr3 = crypto.PubkeyToAddress(key3.PublicKey)
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0) tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3)
blob1 = encodeForPool(tx1) blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2) blob2 = encodeForPool(tx2)
@ -1318,7 +1292,7 @@ func TestLegacyTxConversion(t *testing.T) {
// Initialize the pending store with two blob transactions encoded in the // Initialize the pending store with two blob transactions encoded in the
// legacy format. // legacy format.
queuedir := filepath.Join(storage, pendingTransactionStore) 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 { if err != nil {
t.Fatalf("failed to open billy: %v", err) t.Fatalf("failed to open billy: %v", err)
} }
@ -1328,8 +1302,8 @@ func TestLegacyTxConversion(t *testing.T) {
addr1 := crypto.PubkeyToAddress(key1.PublicKey) addr1 := crypto.PubkeyToAddress(key1.PublicKey)
addr2 := crypto.PubkeyToAddress(key2.PublicKey) addr2 := crypto.PubkeyToAddress(key2.PublicKey)
tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1, types.BlobSidecarVersion0) tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1)
tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2, types.BlobSidecarVersion0) tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2)
for _, tx := range []*types.Transaction{tx1, tx2} { for _, tx := range []*types.Transaction{tx1, tx2} {
legacy, err := rlp.EncodeToBytes(tx) legacy, err := rlp.EncodeToBytes(tx)
@ -1427,8 +1401,8 @@ func TestBlobCountLimit(t *testing.T) {
// Attempt to add transactions. // Attempt to add transactions.
var ( var (
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0) tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1)
tx2 = makeMultiBlobTx(0, 1, 800, 70, 7, 0, key2, types.BlobSidecarVersion0) tx2 = makeMultiBlobTx(0, 1, 800, 70, 7, 0, key2)
) )
errs := pool.Add([]*types.Transaction{tx1, tx2}, true) 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)) storage := filepath.Join(t.TempDir(), fmt.Sprintf("test-%d", i))
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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 // Insert the seed transactions for the pool startup
var ( var (
@ -1941,7 +1915,7 @@ func TestGetBlobs(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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. // Create transactions from a few accounts.
var ( var (
@ -1953,9 +1927,9 @@ func TestGetBlobs(t *testing.T) {
addr2 = crypto.PubkeyToAddress(key2.PublicKey) addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey) addr3 = crypto.PubkeyToAddress(key3.PublicKey)
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0) // [0, 6) tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1) // [0, 6)
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion1) // [6, 12) tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2) // [6, 12)
tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3, types.BlobSidecarVersion0) // [12, 18) tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3) // [12, 18)
blob1 = encodeForPool(tx1) blob1 = encodeForPool(tx1)
blob2 = encodeForPool(tx2) blob2 = encodeForPool(tx2)
@ -2023,7 +1997,7 @@ func TestGetBlobs(t *testing.T) {
}{ }{
{ {
start: 0, limit: 6, start: 0, limit: 6,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 0, limit: 6, start: 0, limit: 6,
@ -2031,7 +2005,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 0, limit: 6, fillRandom: true, start: 0, limit: 6, fillRandom: true,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 0, limit: 6, fillRandom: true, start: 0, limit: 6, fillRandom: true,
@ -2039,7 +2013,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 3, limit: 9, start: 3, limit: 9,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 3, limit: 9, start: 3, limit: 9,
@ -2047,7 +2021,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 3, limit: 9, fillRandom: true, start: 3, limit: 9, fillRandom: true,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 3, limit: 9, fillRandom: true, start: 3, limit: 9, fillRandom: true,
@ -2055,7 +2029,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 3, limit: 15, start: 3, limit: 15,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 3, limit: 15, start: 3, limit: 15,
@ -2063,7 +2037,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 3, limit: 15, fillRandom: true, start: 3, limit: 15, fillRandom: true,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 3, limit: 15, fillRandom: true, start: 3, limit: 15, fillRandom: true,
@ -2071,7 +2045,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 0, limit: 18, start: 0, limit: 18,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 0, limit: 18, start: 0, limit: 18,
@ -2079,7 +2053,7 @@ func TestGetBlobs(t *testing.T) {
}, },
{ {
start: 0, limit: 18, fillRandom: true, start: 0, limit: 18, fillRandom: true,
version: types.BlobSidecarVersion0, version: types.BlobSidecarVersion1,
}, },
{ {
start: 0, limit: 18, fillRandom: true, start: 0, limit: 18, fillRandom: true,
@ -2133,7 +2107,7 @@ func TestGetBlobs(t *testing.T) {
if blobs[j] == nil || proofs[j] == nil { if blobs[j] == nil || proofs[j] == nil {
// This is only an error if there was no version mismatch // This is only an error if there was no version mismatch
if (c.version == types.BlobSidecarVersion1 && 6 <= testBlobIndex && testBlobIndex < 12) || 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]) t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
} }
continue continue
@ -2144,18 +2118,12 @@ func TestGetBlobs(t *testing.T) {
continue continue
} }
// Item retrieved, make sure the proof matches the expectation // 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]) want, _ := kzg4844.ComputeCellProofs(blobs[j])
if !reflect.DeepEqual(want, proofs[j]) { if !reflect.DeepEqual(want, proofs[j]) {
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j]) t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
} }
} }
} }
}
pool.Close() pool.Close()
} }
@ -2166,24 +2134,21 @@ func TestGetBlobs(t *testing.T) {
func TestEncodeForNetwork(t *testing.T) { func TestEncodeForNetwork(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
sidecarVer byte
ethVer uint ethVer uint
}{ }{
{"v0/eth70", types.BlobSidecarVersion0, 70}, {"eth70", 70},
{"v1/eth70", types.BlobSidecarVersion1, 70}, {"eth72", 72},
{"v0/eth72", types.BlobSidecarVersion0, 72},
{"v1/eth72", types.BlobSidecarVersion1, 72},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { 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() key, _ := crypto.GenerateKey()
tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, sidecarVer) tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key)
wantTx := tx wantTx := tx
if ethVer >= 72 { if ethVer >= 72 {
@ -2293,14 +2258,14 @@ func TestGetCells(t *testing.T) {
storage := t.TempDir() storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) 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 ( var (
key1, _ = crypto.GenerateKey() key1, _ = crypto.GenerateKey()
addr1 = crypto.PubkeyToAddress(key1.PublicKey) 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) pooledTx1, _ = newBlobTxForPool(tx1)

View file

@ -13,7 +13,7 @@ import (
// (simulating what ETH/72 peers send). // (simulating what ETH/72 peers send).
func makeV1Tx(t *testing.T, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey) *types.Transaction { func makeV1Tx(t *testing.T, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey) *types.Transaction {
t.Helper() 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) return removeBlobs(tx)
} }

View file

@ -58,7 +58,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache {
if err := os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700); err != nil { if err := os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700); err != nil {
t.Fatalf("mkdir: %v", err) 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 { if err != nil {
t.Fatalf("billy open: %v", err) t.Fatalf("billy open: %v", err)
} }
@ -70,7 +70,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache {
) )
for _, s := range txConfig { for _, s := range txConfig {
key, _ := crypto.GenerateKey() 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 { if _, err := store.Put(encodeForPool(tx)); err != nil {
t.Fatalf("store put: %v", err) 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 { func (tc *testCache) inject(t *testing.T, spec txSpec) []common.Hash {
t.Helper() t.Helper()
key, _ := crypto.GenerateKey() 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 tc.offset += spec.blobs
ptx, err := newBlobTxForPool(tx) ptx, err := newBlobTxForPool(tx)

View file

@ -56,7 +56,7 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
} }
// Create new slotter for pre-Osaka blob configuration. // 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. // See if we need to migrate the limbo after fusaka.
slotter, err := tryMigrate(config, slotter, datadir) slotter, err := tryMigrate(config, slotter, datadir)

View file

@ -58,27 +58,6 @@ func tryMigrate(config *params.ChainConfig, slotter billy.SlotSizeFn, datadir st
return slotter, nil 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. // newSlotterEIP7594 creates a different slotter for EIP-7594 transactions.
// EIP-7594 (PeerDAS) changes the average transaction size which means the current // EIP-7594 (PeerDAS) changes the average transaction size which means the current
// static 4KB average size is not enough anymore. // static 4KB average size is not enough anymore.

View file

@ -20,47 +20,6 @@ import (
"testing" "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. // Tests that the slotter creates the expected database shelves.
func TestNewSlotterEIP7594(t *testing.T) { func TestNewSlotterEIP7594(t *testing.T) {
// Generate the database shelve sizes // Generate the database shelve sizes

View file

@ -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 // Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata // 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 { if err != nil {
return err return err
} }
@ -135,22 +135,29 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if err != nil { if err != nil {
return err return err
} }
// Make sure the transaction has sufficient gas allowance to
// pay the floor cost.
if tx.Gas() < floorDataGas { if tx.Gas() < floorDataGas {
return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, 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 // Ensure the gasprice is high enough to cover the requirement of the calling pool
if tx.GasTipCapIntCmp(opts.MinTip) < 0 { if tx.GasTipCapIntCmp(opts.MinTip) < 0 {
return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip) 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 tx.Type() == types.SetCodeTxType {
if len(tx.SetCodeAuthorizations()) == 0 { if len(tx.SetCodeAuthorizations()) == 0 {
return errors.New("set code tx must have at least one authorization tuple") 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 return nil
} }
@ -172,26 +179,12 @@ func validateBlobSidecar(tx *types.Transaction, head *types.Header, opts *Valida
if len(hashes) > opts.MaxBlobCount { if len(hashes) > opts.MaxBlobCount {
return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, len(hashes), opts.MaxBlobCount) return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, len(hashes), opts.MaxBlobCount)
} }
if sidecar.Version != types.BlobSidecarVersion1 {
expected := types.BlobSidecarVersion0 return fmt.Errorf("%w: unexpected sidecar version, want: %d, got: %d", ErrSidecarFormatError, types.BlobSidecarVersion1, sidecar.Version)
if opts.Config.IsOsaka(head.Number, head.Time) {
expected = types.BlobSidecarVersion1
} }
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 { 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 fmt.Errorf("%w: invalid number of %d blob proofs expected %d", ErrSidecarFormatError, len(sidecar.Proofs), len(sidecar.Commitments)*kzg4844.CellProofsPerBlob)
} }
}
return nil return nil
} }
@ -210,25 +203,11 @@ func ValidateCells(sidecar *types.BlobTxCellSidecar) error {
if blobCount != len(sidecar.Commitments) { if blobCount != len(sidecar.Commitments) {
return fmt.Errorf("invalid number of %d blobs compared to %d commitments", 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 {
if sidecar.Version == types.BlobSidecarVersion1 { return fmt.Errorf("unexpected sidecar version, want: %d, got: %d", types.BlobSidecarVersion1, sidecar.Version)
}
return validateCellsOsaka(sidecar) return validateCellsOsaka(sidecar)
} }
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
}
func validateCellsOsaka(sidecar *types.BlobTxCellSidecar) error { func validateCellsOsaka(sidecar *types.BlobTxCellSidecar) error {
indices := sidecar.Custody.Indices() indices := sidecar.Custody.Indices()

View file

@ -112,6 +112,5 @@ func toWordSize(size uint64) uint64 {
if size > math.MaxUint64-31 { if size > math.MaxUint64-31 {
return math.MaxUint64/32 + 1 return math.MaxUint64/32 + 1
} }
return (size + 31) / 32 return (size + 31) / 32
} }

View file

@ -42,6 +42,8 @@ type Contract struct {
IsDeployment bool IsDeployment bool
IsSystemCall bool IsSystemCall bool
// Gas carries the unified gas state for this frame: running balance,
// reservoir, and per-frame usage accumulators. See GasBudget.
Gas GasBudget Gas GasBudget
value *uint256.Int value *uint256.Int
} }
@ -113,7 +115,6 @@ func (c *Contract) GetOp(n uint64) OpCode {
if n < uint64(len(c.Code)) { if n < uint64(len(c.Code)) {
return OpCode(c.Code[n]) return OpCode(c.Code[n])
} }
return STOP return STOP
} }
@ -125,9 +126,10 @@ func (c *Contract) Caller() common.Address {
return c.caller return c.caller
} }
// UseGas attempts the use gas and subtracts it and returns true on success // chargeRegular deducts regular gas only, with tracer integration.
func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) { // Returns false on OOG. Delegates the arithmetic to GasBudget.ChargeRegular.
prior, ok := c.Gas.Charge(cost) func (c *Contract) chargeRegular(r uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
prior, ok := c.Gas.ChargeRegular(r)
if !ok { if !ok {
return false return false
} }
@ -137,15 +139,44 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G
return true return true
} }
// RefundGas refunds the leftover gas budget back to the contract. // chargeState deducts state gas (spilling into regular when the reservoir is
func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { // exhausted), with tracer integration. Returns false on OOG.
prior, changed := c.Gas.Refund(refund) func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool {
if !changed { prior, ok := c.Gas.ChargeState(s)
return if !ok {
return false
} }
if logger.HasGasHook() && reason != tracing.GasChangeIgnored { if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) 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 // Address returns the contracts address

View file

@ -264,9 +264,8 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
// - any error that occurred // - 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) { 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) gasCost := p.RequiredGas(input)
prior, ok := gas.Charge(GasCosts{RegularGas: gasCost}) prior, ok := gas.ChargeRegular(gasCost)
if !ok { if !ok {
gas.Exhaust()
return nil, gas, ErrOutOfGas return nil, gas, ErrOutOfGas
} }
if logger.HasGasHook() { if logger.HasGasHook() {

View file

@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) {
return return
} }
inWant := string(input) 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 { if inHave := string(input); inWant != inHave {
t.Errorf("Precompiled %v modified input data", a) t.Errorf("Precompiled %v modified input data", a)
} }

View file

@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
in := common.Hex2Bytes(test.Input) in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in) gas := p.RequiredGas(in)
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { 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) t.Error(err)
} else if common.Bytes2Hex(res) != test.Expected { } else if common.Bytes2Hex(res) != test.Expected {
t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) 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 gas := test.Gas - 1
t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { 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" { if err.Error() != "out of gas" {
t.Errorf("Expected error [out of gas], got [%v]", err) 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) in := common.Hex2Bytes(test.Input)
gas := p.RequiredGas(in) gas := p.RequiredGas(in)
t.Run(test.Name, func(t *testing.T) { 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 { if err.Error() != test.ExpectedError {
t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) 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() start := time.Now()
for bench.Loop() { for bench.Loop() {
copy(data, in) 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)) elapsed := uint64(time.Since(start))
if elapsed < 1 { if elapsed < 1 {

View file

@ -43,6 +43,7 @@ var activators = map[int]func(*JumpTable){
7939: enable7939, 7939: enable7939,
8024: enable8024, 8024: enable8024,
7843: enable7843, 7843: enable7843,
8037: enable8037,
} }
// EnableEIP enables the given EIP on the config. // 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 { if evm.readOnly {
return nil, ErrWriteProtection return nil, ErrWriteProtection
} }
loc := scope.Stack.pop() loc, val := scope.Stack.pop2()
val := scope.Stack.pop()
evm.StateDB.SetTransientState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32()) evm.StateDB.SetTransientState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32())
return nil, nil return nil, nil
} }
@ -262,11 +262,7 @@ func enable5656(jt *JumpTable) {
// opMcopy implements the MCOPY opcode (https://eips.ethereum.org/EIPS/eip-5656) // opMcopy implements the MCOPY opcode (https://eips.ethereum.org/EIPS/eip-5656)
func opMcopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opMcopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( dst, src, length := scope.Stack.pop3()
dst = scope.Stack.pop()
src = scope.Stack.pop()
length = scope.Stack.pop()
)
// These values are checked for overflow during memory expansion calculation // These values are checked for overflow during memory expansion calculation
// (the memorySize function on the opcode). // (the memorySize function on the opcode).
scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64()) scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64())
@ -364,10 +360,7 @@ func enable8024(jt *JumpTable) {
func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( var (
stack = scope.Stack stack = scope.Stack
a = stack.pop() a, memOffset, codeOffset, length = stack.pop4()
memOffset = stack.pop()
codeOffset = stack.pop()
length = stack.pop()
) )
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow { if overflow {
@ -377,7 +370,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
code := evm.StateDB.GetCode(addr) code := evm.StateDB.GetCode(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas) 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 { if consumed < wanted {
return nil, ErrOutOfGas return nil, ErrOutOfGas
} }
@ -403,7 +396,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// advanced past this boundary. // advanced past this boundary.
contractAddr := scope.Contract.Address() contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) 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 { if consumed < wanted {
return nil, ErrOutOfGas return nil, ErrOutOfGas
} }
@ -430,7 +423,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc {
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall {
contractAddr := scope.Contract.Address() contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) 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 { if consumed < wanted {
return nil, ErrOutOfGas return nil, ErrOutOfGas
} }
@ -590,3 +583,11 @@ func enable7843(jt *JumpTable) {
maxStack: maxStack(0, 1), 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
}

View file

@ -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) 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 Random *common.Hash // Provides information for PREVRANDAO
SlotNum uint64 // Provides information for SLOTNUM SlotNum uint64 // Provides information for SLOTNUM
CostPerStateByte uint64 // CostPerByte for new state after EIP-8037
} }
// TxContext provides the EVM with information about a transaction. // 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 // 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 // the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer. // 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 // Capture the tracer start/end events in debug mode
if evm.Config.Tracer != nil { if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig()) evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())
defer func(startGas uint64) { defer func(startGas GasBudget) {
evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) evm.captureEnd(evm.depth, startGas, result, ret, err)
}(gas.RegularGas) }(gas)
} }
// Fail if we're trying to execute above the call depth limit // Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) { 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) { if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance return nil, gas, ErrInsufficientBalance
} }
snapshot := evm.StateDB.Snapshot() snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
p, isPrecompile := evm.precompile(addr) p, isPrecompile := evm.precompile(addr)
if !evm.StateDB.Exist(addr) { if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { 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. // hash leaf to the access list, then account creation will proceed unimpaired.
// Thus, only pay for the creation of the code hash leaf here. // Thus, only pay for the creation of the code hash leaf here.
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) 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) evm.StateDB.RevertToSnapshot(snapshot)
gas.Exhaust() return nil, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, gas, ErrOutOfGas
} }
} }
@ -288,6 +289,16 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
} }
evm.StateDB.CreateAccount(addr) 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. // Perform the value transfer only in non-syscall mode.
// Calling this is required even for zero-value transfers, // Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied. // 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 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, // Calculate the remaining gas at the end of frame
// when we're in homestead this also counts for code storage gas errors. exitGas := gas.Exit(err, reservoir)
if err != nil { if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() { 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 // 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' // CallCode differs from Call in the sense that it executes the given address'
// code with the caller as context. // 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 // Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil { if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig()) evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig())
defer func(startGas uint64) { defer func(startGas GasBudget) {
evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) evm.captureEnd(evm.depth, startGas, result, ret, err)
}(gas.RegularGas) }(gas)
} }
// Fail if we're trying to execute above the call depth limit // Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) { if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth return nil, gas, ErrDepth
} }
// Fail if we're trying to transfer more than the available balance // 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) { if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance 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 // It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile { 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) ret, err = evm.Run(contract, input, false)
gas = contract.Gas gas = contract.Gas
} }
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
if err != nil { if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() { 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 // 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' // 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. // 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 // Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil { if evm.Config.Tracer != nil {
// DELEGATECALL inherits value from parent call // DELEGATECALL inherits value from parent call
evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig()) evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig())
defer func(startGas uint64) { defer func(startGas GasBudget) {
evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) evm.captureEnd(evm.depth, startGas, result, ret, err)
}(gas.RegularGas) }(gas)
} }
// Fail if we're trying to execute above the call depth limit // Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) { if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth 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 // It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile { if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else { } 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 := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false) ret, err = evm.Run(contract, input, false)
gas = contract.Gas gas = contract.Gas
} }
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
if err != nil { if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() { 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 // StaticCall executes the contract associated with the addr with the given input
// as parameters while disallowing any modifications to the state during the call. // as parameters while disallowing any modifications to the state during the call.
// Opcodes that attempt to perform such modifications will result in exceptions // Opcodes that attempt to perform such modifications will result in exceptions
// instead of performing the modifications. // 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 // Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Tracer != nil { if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil) evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil)
defer func(startGas uint64) { defer func(startGas GasBudget) {
evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) evm.captureEnd(evm.depth, startGas, result, ret, err)
}(gas.RegularGas) }(gas)
} }
// Fail if we're trying to execute above the call depth limit // Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) { 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, // after all empty accounts were deleted, so this is not required. However, if we omit this,
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json. // then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
// We could change this, but for now it's left for legacy reasons // 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. // 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, // 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 { if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules)
} else { } 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 := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) 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) ret, err = evm.Run(contract, input, true)
gas = contract.Gas gas = contract.Gas
} }
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
if err != nil { if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted { if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() { 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. // 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) { 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) {
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)
}
// Depth check execution. Fail if we're trying to execute above the // Depth check execution. Fail if we're trying to execute above the
// limit. // limit.
var nonce uint64
if evm.depth > int(params.CallCreateDepth) { if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{}, gas, ErrDepth err = ErrDepth
} } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
if !evm.Context.CanTransfer(evm.StateDB, caller, value) { err = ErrInsufficientBalance
return nil, common.Address{}, gas, ErrInsufficientBalance } else {
} nonce = evm.StateDB.GetNonce(caller)
nonce := evm.StateDB.GetNonce(caller)
if nonce+1 < nonce { if nonce+1 < nonce {
return nil, common.Address{}, gas, ErrNonceUintOverflow err = ErrNonceUintOverflow
} }
}
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)
}
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) evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
reservoir := gas.StateGas
// Charge the contract creation init gas in verkle mode // Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 { if evm.chainRules.IsEIP4762 {
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
if !ok { if !ok {
gas.Exhaust() return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas, ErrOutOfGas
} }
if evm.Config.Tracer.HasGasHook() { if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) 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 || if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
halt := gas.ExitHalt(reservoir)
if evm.Config.Tracer.HasGasHook() { 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() // EIP-8037 collision rule: the state reservoir is fully preserved on
return nil, common.Address{}, gas, ErrContractAddressCollision // 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. // 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 // 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() snapshot := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) { if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(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 // CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account. // 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 { if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted { if consumed < wanted {
gas.Exhaust() return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas, ErrOutOfGas
} }
prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() { if evm.Config.Tracer.HasGasHook() {
@ -574,13 +596,23 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
contract.IsDeployment = true contract.IsDeployment = true
ret, err = evm.initNewContract(contract, address) 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) { if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot) evm.StateDB.RevertToSnapshot(snapshot)
exit := contract.Gas.Exit(err, reservoir)
if err != ErrExecutionReverted { 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, contract.Gas, err return ret, address, exit, 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 // 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 { if err != nil {
return ret, err return ret, err
} }
// Check prefix before gas calculation.
// 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
}
// Reject code starting with 0xEF if EIP-3541 is enabled. // Reject code starting with 0xEF if EIP-3541 is enabled.
if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
return ret, ErrInvalidCode return ret, ErrInvalidCode
} }
if evm.chainRules.IsEIP4762 {
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 {
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) 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) { if len(ret) > 0 && (consumed < wanted) {
return ret, ErrCodeStoreOutOfGas 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 { if len(ret) > 0 {
evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation) 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. // 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)) contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE) 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:] // 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. // 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) inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) 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 // ChainConfig returns the environment's chain configuration
func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } 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 tracer := evm.Config.Tracer
if tracer.OnEnter != nil { 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() { if tracer.HasGasHook() {
initial := NewGasBudget(startGas) tracer.EmitGasChange(tracing.Gas{}, startGas.AsTracing(), tracing.GasChangeCallInitialBalance)
tracer.EmitGasChange(tracing.Gas{}, initial.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 tracer := evm.Config.Tracer
if leftOverGas != 0 && tracer.HasGasHook() { if !leftOverGas.IsZero() && tracer.HasGasHook() {
leftover := NewGasBudget(leftOverGas) tracer.EmitGasChange(leftOverGas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
} }
var reverted bool var reverted bool
if err != nil { if err != nil {
@ -693,7 +738,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [
reverted = false reverted = false
} }
if tracer.OnExit != nil { if tracer.OnExit != nil {
tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted) tracer.OnExit(depth, ret, startGas.RegularGas-leftOverGas.RegularGas, VMErrorFromErr(err), reverted)
} }
} }

View file

@ -291,10 +291,19 @@ var (
gasMLoad = pureMemoryGascost gasMLoad = pureMemoryGascost
gasMStore8 = pureMemoryGascost gasMStore8 = pureMemoryGascost
gasMStore = 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) { 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) gas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err 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) { 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) gas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err return GasCosts{}, err
@ -331,7 +343,11 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
} }
return GasCosts{RegularGas: gas}, nil return GasCosts{RegularGas: gas}, nil
} }
func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { 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) gas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err return GasCosts{}, err
@ -384,17 +400,17 @@ var (
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic) 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) { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize) intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err 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 { if err != nil {
return GasCosts{}, err return GasCosts{}, err
} }
gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp) gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
if overflow { if overflow {
return GasCosts{}, ErrGasUintOverflow 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 ( var (
gas uint64 gas uint64
transfersValue = !stack.back(2).IsZero() transfersValue = !stack.back(2).IsZero()
address = common.Address(stack.back(1).Bytes20()) address = common.Address(stack.back(1).Bytes20())
) )
if evm.readOnly && transfersValue { if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection return 0, ErrWriteProtection
} }
// Stateless check // Stateless check
memoryGas, err := memoryGasCost(mem, memorySize) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err return 0, err
} }
var transferGas uint64 var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 { if transfersValue && !evm.chainRules.IsEIP4762 {
@ -422,12 +438,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
} }
var overflow bool var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { 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, // Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps. // it can effectively prevent accessing the states in the following steps.
if contract.Gas.RegularGas < gas { if contract.Gas.RegularGas < gas {
return GasCosts{}, ErrOutOfGas return 0, ErrOutOfGas
} }
// Stateful check // Stateful check
var stateGas uint64 var stateGas uint64
@ -439,15 +455,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
stateGas += params.CallNewAccountGas stateGas += params.CallNewAccountGas
} }
if gas, overflow = math.SafeAdd(gas, stateGas); overflow { 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) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return GasCosts{}, err return 0, err
} }
var ( var (
gas uint64 gas uint64
@ -457,38 +473,36 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor
gas += params.CallValueTransferGas gas += params.CallValueTransferGas
} }
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { 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) gas, err := memoryGasCost(mem, memorySize)
if err != nil { 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) gas, err := memoryGasCost(mem, memorySize)
if err != nil { 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) { func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly { if evm.readOnly {
return GasCosts{}, ErrWriteProtection return GasCosts{}, ErrWriteProtection
} }
var gas uint64 var gas uint64
// EIP150 homestead gas reprice fork: // EIP150 homestead gas reprice fork:
if evm.chainRules.IsEIP150 { if evm.chainRules.IsEIP150 {
gas = params.SelfdestructGasEIP150 gas = params.SelfdestructGasEIP150
var address = common.Address(stack.back(0).Bytes20()) var address = common.Address(stack.back(0).Bytes20())
if evm.chainRules.IsEIP158 { if evm.chainRules.IsEIP158 {
// if empty and transfers value // if empty and transfers value
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { 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 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)
}

View file

@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) {
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
} }
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
initialGas := NewGasBudget(tt.gaspool) initialGas := NewGasBudget(tt.gaspool, 0)
_, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) _, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int))
if !errors.Is(err, tt.failure) { if !errors.Is(err, tt.failure) {
t.Errorf("test %d: failure mismatch: have %v, want %v", i, 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) t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
} }
if refund := evm.StateDB.GetRefund(); refund != tt.refund { if refund := evm.StateDB.GetRefund(); refund != tt.refund {
@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) {
} }
evm := NewEVM(vmctx, statedb, chainConfig, config) evm := NewEVM(vmctx, statedb, chainConfig, config)
initialGas := NewGasBudget(uint64(testGas)) initialGas := NewGasBudget(uint64(testGas), 0)
ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) ret, result, err := evm.Call(common.Address{}, address, nil, initialGas, new(uint256.Int))
if err != nil { if err != nil {
return false return false
} }
gasUsed = leftOver.Used(initialGas) gasUsed = result.Used(initialGas)
if len(ret) != 32 { if len(ret) != 32 {
t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret)) t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret))
} }

View file

@ -20,11 +20,11 @@ import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/log"
) )
// GasCosts denotes a vector of gas costs in the // GasCosts denotes a vector of gas costs in the multidimensional metering
// multidimensional metering paradigm. It represents the cost // paradigm. It represents the cost charged by an individual operation.
// charged by an individual operation.
type GasCosts struct { type GasCosts struct {
RegularGas uint64 RegularGas uint64
StateGas uint64 StateGas uint64
@ -40,67 +40,259 @@ func (g GasCosts) String() string {
return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
} }
// GasBudget denotes a vector of remaining gas allowances available // GasBudget is the unified gas-state structure used throughout the EVM.
// for EVM execution in the multidimensional metering paradigm. // It carries two pairs of fields:
// Unlike GasCosts which represents the price of an operation, //
// GasBudget tracks how much gas is left to spend. // - 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 { type GasBudget struct {
RegularGas uint64 // The leftover gas for execution and state gas usage RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb)
StateGas uint64 // The state gas reservoir 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. // NewGasBudget initializes a fresh GasBudget for execution / forwarding,
func NewGasBudget(gas uint64) GasBudget { // with both usage accumulators set to zero.
return GasBudget{RegularGas: gas} 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 { 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 // String returns a visual representation of the budget.
// for usage tracking.
func (g *GasBudget) Exhaust() {
g.RegularGas = 0
g.StateGas = 0
}
func (g *GasBudget) Copy() GasBudget {
return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas}
}
// String returns a visual representation of the gas budget vector.
func (g GasBudget) String() string { 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. // Charge deducts a combined regular+state cost from the running balance and
func (g GasBudget) CanAfford(cost GasCosts) bool { // updates the usage accumulators. State-gas in excess of the reservoir spills
return g.RegularGas >= cost.RegularGas // into regular_gas.
}
// Charge deducts the given gas cost from the budget. It returns the
// pre-charge budget and false if the budget does not have sufficient
// gas to cover the cost.
func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
prior := *g prior := *g
if g.RegularGas < cost.RegularGas { ok := g.charge(cost)
return prior, false return prior, ok
}
g.RegularGas -= cost.RegularGas
return prior, true
} }
// Refund adds the given gas budget back. It returns the pre-refund budget // chargeRegularOnly deducts a regular-only cost.
// and whether the budget was actually changed. func (g *GasBudget) chargeRegularOnly(r uint64) bool {
func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) { if g.RegularGas < r {
prior := *g return false
g.RegularGas += other.RegularGas }
return prior, g.RegularGas != prior.RegularGas 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. // AsTracing converts the GasBudget into the tracing-facing Gas vector.
func (g GasBudget) AsTracing() tracing.Gas { func (g GasBudget) AsTracing() tracing.Gas {
return tracing.Gas{Regular: g.RegularGas, State: g.StateGas} 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
}

View file

@ -27,56 +27,56 @@ import (
) )
func opAdd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opAdd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Add(&x, y) y.Add(x, y)
return nil, nil return nil, nil
} }
func opSub(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opSub(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Sub(&x, y) y.Sub(x, y)
return nil, nil return nil, nil
} }
func opMul(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opMul(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Mul(&x, y) y.Mul(x, y)
return nil, nil return nil, nil
} }
func opDiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opDiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Div(&x, y) y.Div(x, y)
return nil, nil return nil, nil
} }
func opSdiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opSdiv(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.SDiv(&x, y) y.SDiv(x, y)
return nil, nil return nil, nil
} }
func opMod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opMod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Mod(&x, y) y.Mod(x, y)
return nil, nil return nil, nil
} }
func opSmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opSmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.SMod(&x, y) y.SMod(x, y)
return nil, nil return nil, nil
} }
func opExp(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opExp(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
base, exponent := scope.Stack.pop(), scope.Stack.peek() base, exponent := scope.Stack.pop1Peek1()
exponent.Exp(&base, exponent) exponent.Exp(base, exponent)
return nil, nil return nil, nil
} }
func opSignExtend(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opSignExtend(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
back, num := scope.Stack.pop(), scope.Stack.peek() back, num := scope.Stack.pop1Peek1()
num.ExtendSign(num, &back) num.ExtendSign(num, back)
return nil, nil 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) { 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) { if x.Lt(y) {
y.SetOne() y.SetOne()
} else { } 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) { 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) { if x.Gt(y) {
y.SetOne() y.SetOne()
} else { } 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) { 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) { if x.Slt(y) {
y.SetOne() y.SetOne()
} else { } 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) { 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) { if x.Sgt(y) {
y.SetOne() y.SetOne()
} else { } 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) { 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) { if x.Eq(y) {
y.SetOne() y.SetOne()
} else { } 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) { func opAnd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.And(&x, y) y.And(x, y)
return nil, nil return nil, nil
} }
func opOr(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opOr(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Or(&x, y) y.Or(x, y)
return nil, nil return nil, nil
} }
func opXor(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opXor(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y := scope.Stack.pop(), scope.Stack.peek() x, y := scope.Stack.pop1Peek1()
y.Xor(&x, y) y.Xor(x, y)
return nil, nil return nil, nil
} }
func opByte(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opByte(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
th, val := scope.Stack.pop(), scope.Stack.peek() th, val := scope.Stack.pop1Peek1()
val.Byte(&th) val.Byte(th)
return nil, nil return nil, nil
} }
func opAddmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opAddmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek() x, y, z := scope.Stack.pop2Peek1()
z.AddMod(&x, &y, z) z.AddMod(x, y, z)
return nil, nil return nil, nil
} }
func opMulmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opMulmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek() x, y, z := scope.Stack.pop2Peek1()
z.MulMod(&x, &y, z) z.MulMod(x, y, z)
return nil, nil 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. // 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) { 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 // 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) { if shift.LtUint64(256) {
value.Lsh(value, uint(shift.Uint64())) value.Lsh(value, uint(shift.Uint64()))
} else { } 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. // 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) { 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 // 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) { if shift.LtUint64(256) {
value.Rsh(value, uint(shift.Uint64())) value.Rsh(value, uint(shift.Uint64()))
} else { } 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, // 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. // 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) { 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 shift.GtUint64(256) {
if value.Sign() >= 0 { if value.Sign() >= 0 {
value.Clear() 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) { 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()) data := scope.Memory.GetPtr(offset.Uint64(), size.Uint64())
hash := crypto.Keccak256Hash(data) 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) { func opCallDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( memOffset, dataOffset, length := scope.Stack.pop3()
memOffset = scope.Stack.pop()
dataOffset = scope.Stack.pop()
length = scope.Stack.pop()
)
dataOffset64, overflow := dataOffset.Uint64WithOverflow() dataOffset64, overflow := dataOffset.Uint64WithOverflow()
if overflow { if overflow {
dataOffset64 = math.MaxUint64 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) { func opReturnDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( memOffset, dataOffset, length := scope.Stack.pop3()
memOffset = scope.Stack.pop()
dataOffset = scope.Stack.pop()
length = scope.Stack.pop()
)
offset64, overflow := dataOffset.Uint64WithOverflow() offset64, overflow := dataOffset.Uint64WithOverflow()
if overflow { 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) // we can reuse dataOffset now (aliasing it for clarity)
var end = dataOffset var end = dataOffset
end.Add(&dataOffset, &length) end.Add(dataOffset, length)
end64, overflow := end.Uint64WithOverflow() end64, overflow := end.Uint64WithOverflow()
if overflow || uint64(len(evm.returnData)) < end64 { if overflow || uint64(len(evm.returnData)) < end64 {
return nil, ErrReturnDataOutOfBounds 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) { func opCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( memOffset, codeOffset, length := scope.Stack.pop3()
memOffset = scope.Stack.pop()
codeOffset = scope.Stack.pop()
length = scope.Stack.pop()
)
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow { if overflow {
uint64CodeOffset = math.MaxUint64 uint64CodeOffset = math.MaxUint64
@ -360,10 +348,7 @@ func opCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
func opExtCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opExtCodeCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( var (
stack = scope.Stack stack = scope.Stack
a = stack.pop() a, memOffset, codeOffset, length = stack.pop4()
memOffset = stack.pop()
codeOffset = stack.pop()
length = stack.pop()
) )
uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow()
if overflow { 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) { func opPop(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.pop() scope.Stack.drop()
return nil, nil 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) { func opMstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
mStart, val := scope.Stack.pop(), scope.Stack.pop() mStart, val := scope.Stack.pop2()
scope.Memory.Set32(mStart.Uint64(), &val) scope.Memory.Set32(mStart.Uint64(), val)
return nil, nil return nil, nil
} }
func opMstore8(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { 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()) scope.Memory.store[off.Uint64()] = byte(val.Uint64())
return nil, nil return nil, nil
} }
@ -515,8 +500,7 @@ func opSstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.readOnly { if evm.readOnly {
return nil, ErrWriteProtection return nil, ErrWriteProtection
} }
loc := scope.Stack.pop() loc, val := scope.Stack.pop2()
val := scope.Stack.pop()
evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32()) evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32())
return nil, nil return nil, nil
} }
@ -525,8 +509,8 @@ func opJump(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.abort.Load() { if evm.abort.Load() {
return nil, errStopToken return nil, errStopToken
} }
pos := scope.Stack.pop() pos := scope.Stack.pop1()
if !scope.Contract.validJumpdest(&pos) { if !scope.Contract.validJumpdest(pos) {
return nil, ErrInvalidJump return nil, ErrInvalidJump
} }
*pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop *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() { if evm.abort.Load() {
return nil, errStopToken return nil, errStopToken
} }
pos, cond := scope.Stack.pop(), scope.Stack.pop() pos, cond := scope.Stack.pop2()
if !cond.IsZero() { if !cond.IsZero() {
if !scope.Contract.validJumpdest(&pos) { if !scope.Contract.validJumpdest(pos) {
return nil, ErrInvalidJump return nil, ErrInvalidJump
} }
*pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop *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) { func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.readOnly {
return nil, ErrWriteProtection
}
var ( var (
value = scope.Stack.pop() value = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) 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 { if evm.chainRules.IsEIP150 {
gas -= gas / 64 forward -= forward / 64
} }
// reuse size int for stackvalue // reuse size int for stackvalue
stackvalue := size stackvalue := size
scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value)
// Push item on the stack based on the returned error. If the ruleset is // Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only // homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must // 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.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 { if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer 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) { func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if evm.readOnly {
return nil, ErrWriteProtection
}
var ( var (
endowment = scope.Stack.pop() endowment = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop()
salt = scope.Stack.pop() salt = scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) 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 // reuse size int for stackvalue
stackvalue := size stackvalue := size
res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas), child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
&endowment, &salt) res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
// Push item on the stack based on the returned error. // Push item on the stack based on the returned error.
if suberr != nil { if suberr != nil {
stackvalue.Clear() stackvalue.Clear()
@ -715,7 +693,9 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
stackvalue.SetBytes(addr.Bytes()) stackvalue.SetBytes(addr.Bytes())
} }
scope.Stack.push(&stackvalue) 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 { if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer 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() { if !value.IsZero() {
gas += params.CallStipend 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 { if err != nil {
temp.Clear() temp.Clear()
@ -751,11 +736,11 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
temp.SetOne() temp.SetOne()
} }
stack.push(&temp) stack.push(&temp)
if err == nil || err == ErrExecutionReverted { if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
} }
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret evm.returnData = ret
return ret, nil return ret, nil
@ -776,8 +761,11 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if !value.IsZero() { if !value.IsZero() {
gas += params.CallStipend gas += params.CallStipend
} }
// Regular gas for the forward was already pre-deducted by the dynamic
ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) // 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 { if err != nil {
temp.Clear() temp.Clear()
} else { } else {
@ -788,7 +776,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) 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 evm.returnData = ret
return ret, nil return ret, nil
@ -806,7 +794,11 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory. // Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) 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 { if err != nil {
temp.Clear() temp.Clear()
} else { } else {
@ -816,8 +808,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted { if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
} }
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret evm.returnData = ret
return ret, nil return ret, nil
@ -835,7 +826,11 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// Get arguments from the memory. // Get arguments from the memory.
args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) 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 { if err != nil {
temp.Clear() temp.Clear()
} else { } else {
@ -846,21 +841,21 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) 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 evm.returnData = ret
return ret, nil return ret, nil
} }
func opReturn(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { 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()) ret := scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
return ret, errStopToken return ret, errStopToken
} }
func opRevert(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { 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()) ret := scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
evm.returnData = ret evm.returnData = ret
@ -882,7 +877,7 @@ func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var ( var (
this = scope.Contract.Address() this = scope.Contract.Address()
balance = evm.StateDB.GetBalance(this) balance = evm.StateDB.GetBalance(this)
top = scope.Stack.pop() top = scope.Stack.pop1()
beneficiary = common.Address(top.Bytes20()) beneficiary = common.Address(top.Bytes20())
) )
// The funds are burned immediately if the beneficiary is the caller itself, // 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 ( var (
this = scope.Contract.Address() this = scope.Contract.Address()
balance = evm.StateDB.GetBalance(this) balance = evm.StateDB.GetBalance(this)
top = scope.Stack.pop() top = scope.Stack.pop1()
beneficiary = common.Address(top.Bytes20()) beneficiary = common.Address(top.Bytes20())
newContract = evm.StateDB.IsNewContract(this) newContract = evm.StateDB.IsNewContract(this)
) )
@ -1081,9 +1076,9 @@ func makeLog(size int) executionFunc {
} }
topics := make([]common.Hash, size) topics := make([]common.Hash, size)
stack := scope.Stack stack := scope.Stack
mStart, mSize := stack.pop(), stack.pop() mStart, mSize := stack.pop2()
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
addr := stack.pop() addr := stack.pop1()
topics[i] = addr.Bytes32() topics[i] = addr.Bytes32()
} }

View file

@ -724,7 +724,7 @@ func TestRandom(t *testing.T) {
) )
opRandom(&pc, evm, &ScopeContext{nil, stack, nil}) opRandom(&pc, evm, &ScopeContext{nil, stack, nil})
if have, want := stack.len(), 1; have != want { 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() actual := stack.pop()
expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes())) expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes()))

View file

@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// associated costs. // associated costs.
contractAddr := contract.Address() contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas) 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 { if consumed < wanted {
return nil, ErrOutOfGas 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} return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
} }
// for tracing: this gas consumption event is emitted below in the debug section. // 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 return nil, ErrOutOfGas
} else {
contract.Gas.RegularGas -= cost
} }
// All ops with a dynamic memory usage also has a dynamic gas 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 { if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
} }
// for tracing: this gas consumption event is emitted below in the debug section. if dynamicCost.StateGas == 0 {
if contract.Gas.RegularGas < dynamicCost.RegularGas { if !contract.Gas.chargeRegularOnly(dynamicCost.RegularGas) {
return nil, ErrOutOfGas
}
} else if !contract.Gas.charge(dynamicCost) {
return nil, ErrOutOfGas return nil, ErrOutOfGas
} else {
contract.Gas.RegularGas -= dynamicCost.RegularGas
} }
} }

View file

@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) {
timeout := make(chan bool) timeout := make(chan bool)
go func(evm *EVM) { 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 errChannel <- err
}(evm) }(evm)
@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) {
value = uint256.NewInt(0) value = uint256.NewInt(0)
stack = newStackForTesting() stack = newStackForTesting()
mem = NewMemory() 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))
stack.push(uint256.NewInt(123)) stack.push(uint256.NewInt(123))

View file

@ -25,6 +25,7 @@ import (
type ( type (
executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) 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 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 returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool) memorySizeFunc func(*Stack) (size uint64, overflow bool)
) )
@ -97,6 +98,7 @@ func newAmsterdamInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet() instructionSet := newOsakaInstructionSet()
enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode)
enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE)
enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase)
return validate(instructionSet) return validate(instructionSet)
} }

View file

@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
evm.StateDB.AddAddressToAccessList(addr) evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available // Charge the remaining difference here already, to correctly calculate available
// gas for call // 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 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) 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) { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var ( var (
eip2929Cost uint64 eip2929Cost uint64
@ -295,7 +295,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Charge the remaining difference here already, to correctly calculate // Charge the remaining difference here already, to correctly calculate
// available gas for call // 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 return GasCosts{}, ErrOutOfGas
} }
} }
@ -312,7 +312,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Terminate the gas measurement if the leftover gas is not sufficient, // Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps. // it can effectively prevent accessing the states in the following steps.
// It's an essential safeguard before any stateful check. // It's an essential safeguard before any stateful check.
if contract.Gas.RegularGas < intrinsicCost.RegularGas { if contract.Gas.RegularGas < intrinsicCost {
return GasCosts{}, ErrOutOfGas return GasCosts{}, ErrOutOfGas
} }
@ -324,13 +324,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
evm.StateDB.AddAddressToAccessList(target) evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = params.ColdAccountAccessCostEIP2929 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 return GasCosts{}, ErrOutOfGas
} }
} }
// Calculate the gas budget for the nested call. The costs defined by // Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied. // 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 { if err != nil {
return GasCosts{}, err 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 // part of the dynamic gas. This will ensure it is correctly reported to
// tracers. // tracers.
contract.Gas.RegularGas += eip2929Cost + eip7702Cost 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, // Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
// the CALL opcode itself, and the cost incurred by nested calls. // 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 { if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return GasCosts{}, ErrGasUintOverflow return GasCosts{}, ErrGasUintOverflow
} }
if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow { if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
return GasCosts{}, ErrGasUintOverflow return GasCosts{}, ErrGasUintOverflow
} }
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {

View file

@ -19,6 +19,7 @@ package runtime
import ( import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -40,6 +41,7 @@ func NewEnv(cfg *Config) *vm.EVM {
BaseFee: cfg.BaseFee, BaseFee: cfg.BaseFee,
BlobBaseFee: cfg.BlobBaseFee, BlobBaseFee: cfg.BlobBaseFee,
Random: cfg.Random, Random: cfg.Random,
CostPerStateByte: params.CostPerStateByte,
} }
evm := vm.NewEVM(blockContext, cfg.State, cfg.ChainConfig, cfg.EVMConfig) evm := vm.NewEVM(blockContext, cfg.State, cfg.ChainConfig, cfg.EVMConfig)

View file

@ -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. // set the receiver's (the executing contract) code for execution.
cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified)
// Call the code with the given configuration. // Call the code with the given configuration.
ret, leftOverGas, err := vmenv.Call( ret, result, err := vmenv.Call(
cfg.Origin, cfg.Origin,
common.BytesToAddress([]byte("contract")), common.BytesToAddress([]byte("contract")),
input, input,
vm.NewGasBudget(cfg.GasLimit), vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { 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 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) // - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration. // Call the code with the given configuration.
code, address, leftOverGas, err := vmenv.Create( code, address, result, err := vmenv.Create(
cfg.Origin, cfg.Origin,
input, input,
vm.NewGasBudget(cfg.GasLimit), vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { 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 // 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) statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration. // Call the code with the given configuration.
ret, leftOverGas, err := vmenv.Call( ret, result, err := vmenv.Call(
cfg.Origin, cfg.Origin,
address, address,
input, input,
vm.NewGasBudget(cfg.GasLimit), vm.NewGasBudget(cfg.GasLimit, 0),
uint256.MustFromBig(cfg.Value), uint256.MustFromBig(cfg.Value),
) )
if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { 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
} }

View file

@ -121,6 +121,62 @@ func (s *Stack) len() int {
return s.size 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() { 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] 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]
} }

View file

@ -1936,111 +1936,6 @@ func newGetBlobEnv(t testing.TB, version byte, custody types.CustodyBitmap) (*no
api := newConsensusAPIWithoutHeartbeat(ethServ) api := newConsensusAPIWithoutHeartbeat(ethServ)
return n, api 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) { func TestGetBlobsV2And3(t *testing.T) {
n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll) n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll)
defer n.Close() defer n.Close()

View file

@ -17,6 +17,7 @@
package catalyst package catalyst
import ( import (
"context"
"errors" "errors"
"github.com/ethereum/go-ethereum/beacon/engine" "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 { if api.eth.BlockChain().CurrentBlock().Hash() != parentHash {
return nil, errors.New("parentHash is not current head") 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 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 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 // 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 var err error
txs, err = engine.DecodeTransactions(dec) txs, err = engine.DecodeTransactions(dec)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
} }
extra := make([]byte, 0) extra := make([]byte, 0)

View file

@ -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")
}
})
}

View file

@ -297,9 +297,11 @@ func (d *Downloader) fetchHeaders(from uint64) error {
return err return err
} }
// If the pivot became stale (older than 2*64-8 (bit of wiggle room)), // 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() 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 { if head.Number.Uint64() > d.pivotHeader.Number.Uint64()+2*uint64(fsMinFullBlocks)-8 {
// Retrieve the next pivot header, either from skeleton chain // Retrieve the next pivot header, either from skeleton chain
// or the filled chain // or the filled chain

View file

@ -295,12 +295,19 @@ func (d *Downloader) Progress() ethereum.SyncProgress {
SyncedBytecodeBytes: uint64(progress.BytecodeBytes), SyncedBytecodeBytes: uint64(progress.BytecodeBytes),
SyncedStorage: progress.StorageSynced, SyncedStorage: progress.StorageSynced,
SyncedStorageBytes: uint64(progress.StorageBytes), SyncedStorageBytes: uint64(progress.StorageBytes),
// Snap/1 progress fields
HealedTrienodes: progress.TrienodeHealSynced, HealedTrienodes: progress.TrienodeHealSynced,
HealedTrienodeBytes: uint64(progress.TrienodeHealBytes), HealedTrienodeBytes: uint64(progress.TrienodeHealBytes),
HealedBytecodes: progress.BytecodeHealSynced, HealedBytecodes: progress.BytecodeHealSynced,
HealedBytecodeBytes: uint64(progress.BytecodeHealBytes), HealedBytecodeBytes: uint64(progress.BytecodeHealBytes),
HealingTrienodes: progress.HealingTrienodes, HealingTrienodes: progress.HealingTrienodes,
HealingBytecode: progress.HealingBytecode, 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 { if mode == ethconfig.SnapSync && pivot == nil {
pivot = d.blockchain.CurrentBlock() 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() height := latest.Number.Uint64()
// In beacon mode, use the skeleton chain for the ancestor lookup // 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. // the results in the meantime.
// //
// Note, there's no issue with memory piling up since after 64 blocks the // 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 ( var (
oldPivot *fetchResult // Locked in pivot block, might change eventually oldPivot *fetchResult // Locked in pivot block, might change eventually
oldTail []*fetchResult // Downloaded content after the pivot oldTail []*fetchResult // Downloaded content after the pivot
@ -978,11 +999,15 @@ func (d *Downloader) processSnapSyncContent() error {
return err return err
} }
if P != nil { 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 { if oldPivot != P {
// 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.Cancel()
sync = d.syncState(P.Header) sync = d.syncState(P.Header)
go closeOnErr(sync) go closeOnErr(sync)
}
oldPivot = P oldPivot = P
} }
// Wait for completion, occasionally checking for pivot staleness // 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. // snap/2 syncer skips snap/1-only peers, which cannot answer its BAL requests.
func (d *Downloader) RegisterSnapPeer(p *snap.Peer) error { func (d *Downloader) RegisterSnapPeer(p *snap.Peer) error {
if p.Version() < d.snapSyncer.Version() { 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 nil
} }
return d.snapSyncer.Register(p) 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 // 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 // RegisterSnapPeer's version gate: a peer below the active syncer's version was
// never registered, so there is nothing to remove. // never registered, so there is nothing to remove.

View file

@ -38,4 +38,8 @@ var (
receiptTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/timeout", nil) receiptTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/timeout", nil)
throttleCounter = metrics.NewRegisteredCounter("eth/downloader/throttle", 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)
) )

View file

@ -294,11 +294,17 @@ func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error {
} }
reject := false // reserved peer slots reject := false // reserved peer slots
if h.downloader.ConfigSyncMode() == ethconfig.SnapSync { if h.downloader.ConfigSyncMode() == ethconfig.SnapSync {
if snap == nil { // A peer is useful to the active state syncer only if it offers the snap
// If we are running snap-sync, we want to reserve roughly half the peer // extension at (or above) the syncer's version. Non-snap peers AND peers
// slots for peers supporting the snap protocol. // stuck on an older snap version (e.g. snap/1 while we sync via snap/2)
// The logic here is; we only allow up to 5 more non-snap peers than snap-peers. // are both "non-usable": the node still serves them, but they must not be
if all, snp := h.peers.len(), h.peers.snapLen(); all-snp > snp+5 { // 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 reject = true
} }
} }

View file

@ -50,7 +50,6 @@ var (
// the `eth` protocol, with or without the `snap` extension. // the `eth` protocol, with or without the `snap` extension.
type peerSet struct { type peerSet struct {
peers map[string]*ethPeer // Peers connected on the `eth` protocol peers map[string]*ethPeer // Peers connected on the `eth` protocol
snapPeers int // Number of `snap` compatible peers for connection prioritization
snapWait map[string]chan *snap.Peer // Peers connected on `eth` waiting for their snap extension 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` 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 { if ext != nil {
eth.snapExt = &snapPeer{ext} eth.snapExt = &snapPeer{ext}
ps.snapPeers++
} }
ps.peers[id] = eth ps.peers[id] = eth
return nil return nil
@ -173,14 +171,10 @@ func (ps *peerSet) unregisterPeer(id string) error {
ps.lock.Lock() ps.lock.Lock()
defer ps.lock.Unlock() defer ps.lock.Unlock()
peer, ok := ps.peers[id] if _, ok := ps.peers[id]; !ok {
if !ok {
return errPeerNotRegistered return errPeerNotRegistered
} }
delete(ps.peers, id) delete(ps.peers, id)
if peer.snapExt != nil {
ps.snapPeers--
}
return nil return nil
} }
@ -210,12 +204,21 @@ func (ps *peerSet) len() int {
return len(ps.peers) return len(ps.peers)
} }
// snapLen returns if the current number of `snap` peers in the set. // snapLen returns the number of `snap` peers whose negotiated version is at
func (ps *peerSet) snapLen() int { // 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() ps.lock.RLock()
defer ps.lock.RUnlock() 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. // close disconnects all peers.

View file

@ -861,7 +861,7 @@ func testGetPooledTransaction(t *testing.T, blobTx bool) {
emptyBlob = kzg4844.Blob{} emptyBlob = kzg4844.Blob{}
emptyBlobs = []kzg4844.Blob{emptyBlob} emptyBlobs = []kzg4844.Blob{emptyBlob}
emptyBlobCommit, _ = kzg4844.BlobToCommitment(&emptyBlob) emptyBlobCommit, _ = kzg4844.BlobToCommitment(&emptyBlob)
emptyBlobProof, _ = kzg4844.ComputeBlobProof(&emptyBlob, emptyBlobCommit) emptyCellProof, _ = kzg4844.ComputeCellProofs(&emptyBlob)
emptyBlobHash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit) emptyBlobHash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit)
) )
backend := newTestBackendWithGenerator(0, true, true, nil) backend := newTestBackendWithGenerator(0, true, true, nil)
@ -885,7 +885,7 @@ func testGetPooledTransaction(t *testing.T, blobTx bool) {
To: testAddr, To: testAddr,
BlobHashes: []common.Hash{emptyBlobHash}, BlobHashes: []common.Hash{emptyBlobHash},
BlobFeeCap: uint256.MustFromBig(common.Big1), 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 { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -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 // 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 // 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 // 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 { func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) error {
// Iterate over all accounts in the access list // Iterate over all accounts in the access list
for _, access := range *b { for _, access := range *b {
@ -113,7 +113,7 @@ func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) er
rawdb.DeleteStorageSnapshot(batch, accountHash, storageHash) rawdb.DeleteStorageSnapshot(batch, accountHash, storageHash)
} else { } else {
// Store the slot in the same encoding the snapshot and the // 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 // (leading zeros trimmed), matching core/state's snapshot
// writes. // writes.
blob, _ := rlp.EncodeToBytes(value.Bytes()) blob, _ := rlp.EncodeToBytes(value.Bytes())
@ -176,7 +176,7 @@ func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) er
case isEmpty && !isNew: case isEmpty && !isNew:
// Existing account got fully drained (e.g., pre-funded // Existing account got fully drained (e.g., pre-funded
// address that gets deployed to with init code that // 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. // doesn't pick it up as an empty leaf.
rawdb.DeleteAccountSnapshot(batch, accountHash) rawdb.DeleteAccountSnapshot(batch, accountHash)
default: default:

View file

@ -157,7 +157,7 @@ func TestAccessListApplication(t *testing.T) {
// Verify storage updated. Slots are stored in the canonical snapshot // Verify storage updated. Slots are stored in the canonical snapshot
// encoding (RLP of the value with leading zeros trimmed), the same form // 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) storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
wantStorage, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0x02").Bytes())) wantStorage, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0x02").Bytes()))
if !bytes.Equal(storageVal, wantStorage) { if !bytes.Equal(storageVal, wantStorage) {
@ -328,7 +328,6 @@ func TestAccessListApplicationSkipsUnfetched(t *testing.T) {
Next: unfetchedHash, Next: unfetchedHash,
Last: common.MaxHash, Last: common.MaxHash,
SubTasks: make(map[common.Hash][]*storageTaskV2), SubTasks: make(map[common.Hash][]*storageTaskV2),
stateCompleted: make(map[common.Hash]struct{}),
}} }}
cb := bal.NewConstructionBlockAccessList() cb := bal.NewConstructionBlockAccessList()
@ -369,7 +368,6 @@ func TestAccessListApplicationSkipsUnfetchedStorage(t *testing.T) {
Next: unfetchedHash, Next: unfetchedHash,
Last: common.MaxHash, Last: common.MaxHash,
SubTasks: make(map[common.Hash][]*storageTaskV2), SubTasks: make(map[common.Hash][]*storageTaskV2),
stateCompleted: make(map[common.Hash]struct{}),
}} }}
// BAL touches an unfetched account with a storage write AND an empty // BAL touches an unfetched account with a storage write AND an empty
@ -425,7 +423,6 @@ func TestAccessListApplicationPartialStorage(t *testing.T) {
Last: common.MaxHash, Last: common.MaxHash,
}}, }},
}, },
stateCompleted: make(map[common.Hash]struct{}),
}} }}
cb := bal.NewConstructionBlockAccessList() cb := bal.NewConstructionBlockAccessList()

View file

@ -84,8 +84,10 @@ type Backend interface {
// otherwise only the default (snap/1) versions are offered on the wire. // otherwise only the default (snap/1) versions are offered on the wire.
func MakeProtocols(backend Backend, snapV2 bool) []p2p.Protocol { func MakeProtocols(backend Backend, snapV2 bool) []p2p.Protocol {
versions := ProtocolVersions versions := ProtocolVersions
if snapV2 { if !snapV2 {
versions = append([]uint{SNAP2}, versions...) // 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)) protocols := make([]p2p.Protocol, len(versions))
for i, version := range versions { for i, version := range versions {

View file

@ -186,8 +186,8 @@ func TestSyncProgressV1Discarded(t *testing.T) {
syncer := newSyncerV2(db, rawdb.HashScheme) syncer := newSyncerV2(db, rawdb.HashScheme)
syncer.loadSyncStatus() syncer.loadSyncStatus()
if syncer.previousPivot != nil { if syncer.pivot != nil {
t.Fatalf("expected previousPivot nil after discarding old format, got %+v", syncer.previousPivot) t.Fatalf("expected pivot nil after discarding old format, got %+v", syncer.pivot)
} }
if len(syncer.tasks) != accountConcurrency { if len(syncer.tasks) != accountConcurrency {
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks)) 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 := newSyncerV2(db, rawdb.HashScheme)
syncer.loadSyncStatus() syncer.loadSyncStatus()
if syncer.previousPivot != nil { if syncer.pivot != nil {
t.Fatalf("expected previousPivot nil after corrupt payload, got %+v", syncer.previousPivot) t.Fatalf("expected pivot nil after corrupt payload, got %+v", syncer.pivot)
} }
if len(syncer.tasks) != accountConcurrency { if len(syncer.tasks) != accountConcurrency {
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks)) t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks))

View file

@ -35,11 +35,10 @@ const (
// devp2p capability negotiation. // devp2p capability negotiation.
const ProtocolName = "snap" const ProtocolName = "snap"
// ProtocolVersions are the supported versions of the `snap` protocol advertised // ProtocolVersions are all the `snap` protocol versions this node implements
// by default (first is primary). snap/2 is not safe to advertise unconditionally // (first is primary). What's actually advertised on the wire is decided by
// yet, so it is gated behind a feature flag and appended in MakeProtocols rather // MakeProtocols, which gates snap/2 behind a feature flag.
// than listed here. var ProtocolVersions = []uint{SNAP2, SNAP1}
var ProtocolVersions = []uint{SNAP1}
// protocolLengths are the number of implemented messages corresponding to // protocolLengths are the number of implemented messages corresponding to
// different protocol versions. snap/2 adds GetAccessLists/AccessLists (0x08/0x09). // different protocol versions. snap/2 adds GetAccessLists/AccessLists (0x08/0x09).

View file

@ -41,6 +41,11 @@ type Progress struct {
BytecodeHealBytes common.StorageSize BytecodeHealBytes common.StorageSize
HealingTrienodes uint64 HealingTrienodes uint64
HealingBytecode 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) // 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 OnTrieNodes(peer SyncPeerV2, id uint64, trienodes [][]byte) error
OnAccessLists(peer SyncPeerV2, id uint64, lists rlp.RawList[rlp.RawValue]) 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 is the snap protocol version this syncer implements.
Version() uint Version() uint
} }
@ -122,6 +131,11 @@ func (syncerV1Adapter) OnAccessLists(SyncPeerV2, uint64, rlp.RawList[rlp.RawValu
// Version is SNAP1 // Version is SNAP1
func (syncerV1Adapter) Version() uint { return 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 // 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 // already take SyncPeerV2 and its Sync already takes a header, so only Progress
// (different return type) and OnTrieNodes (absent) need wrapping. // (different return type) and OnTrieNodes (absent) need wrapping.
@ -136,6 +150,9 @@ func (s syncerV2Adapter) Progress() Progress {
BytecodeBytes: progress.BytecodeBytes, BytecodeBytes: progress.BytecodeBytes,
StorageSynced: progress.StorageSynced, StorageSynced: progress.StorageSynced,
StorageBytes: progress.StorageBytes, StorageBytes: progress.StorageBytes,
AccessListSynced: progress.AccessListSynced,
AccessListTotal: progress.AccessListTotal,
TrieGenPercent: progress.TrieGenPercent,
} }
} }

View file

@ -26,6 +26,7 @@ import (
"math/rand" "math/rand"
"sort" "sort"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -37,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/msgrate" "github.com/ethereum/go-ethereum/p2p/msgrate"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
@ -54,6 +56,22 @@ const (
// the assumption that the gas limit is 60M. // the assumption that the gas limit is 60M.
maxAccessListRequestCount = 28 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 // syncProgressVersion is the version byte prepended to the JSON-encoded
// syncProgressV2 when persisted. On load, a mismatching version byte causes // syncProgressV2 when persisted. On load, a mismatching version byte causes
// the persisted progress to be discarded and sync to start fresh. // the persisted progress to be discarded and sync to start fresh.
@ -68,6 +86,10 @@ const (
// are still hashes left to fetch. // are still hashes left to fetch.
var errAccessListPeersExhausted = errors.New("all peers exhausted for BAL requests") 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 // accountRequestV2 tracks a pending account range request to ensure responses are
// to actual requests and to validate any security constraints. // 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 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 // 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 // sync. Opposed to full and fast sync, there is no way to restart a suspended
// snap sync without prior knowledge of the suspension point. // snap sync without prior knowledge of the suspension point.
type syncProgressV2 struct { type syncProgressV2 struct {
Pivot *types.Header // Pivot header being synced (for pivot move and reorg detection) Pivot *types.Header // Pivot header being synced (for pivot move and reorg detection)
Tasks []*accountTaskV2 // The suspended account tasks (contract tasks within) Tasks []*accountTaskV2 // The suspended account tasks (contract tasks within)
Complete bool // True once sync ran to completion for Pivot Phase syncPhase // Phase is how far the sync has progressed for Pivot
// Status report during syncing phase // Status report during syncing phase
AccountSynced uint64 // Number of accounts downloaded AccountSynced uint64 // Number of accounts downloaded
@ -276,6 +319,10 @@ type syncProgressV2 struct {
BytecodeBytes common.StorageSize // Number of bytecode bytes downloaded BytecodeBytes common.StorageSize // Number of bytecode bytes downloaded
StorageSynced uint64 // Number of storage slots downloaded StorageSynced uint64 // Number of storage slots downloaded
StorageBytes common.StorageSize // Number of storage trie bytes persisted to disk 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 // 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 // syncerV2 is an Ethereum account and storage trie syncer based on the snap
// protocol. It downloads all accounts, storage slots, and bytecodes from // protocol. It downloads all accounts, storage slots, and bytecodes from
// remote peers as flat state, applies BAL diffs on pivot moves, // 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: // Every network request has a variety of failure events:
// - The peer disconnects after task assignment, failing to send the request // - The peer disconnects after task assignment, failing to send the request
@ -324,10 +371,8 @@ type SyncPeerV2 interface {
type syncerV2 struct { type syncerV2 struct {
db ethdb.Database // Database to store the trie nodes into (and dedup) db ethdb.Database // Database to store the trie nodes into (and dedup)
scheme string // Node scheme used in node database scheme string // Node scheme used in node database
pivot *types.Header // Current pivot header being synced (lock needed) pivot *types.Header // Current pivot header being synced (lock needed)
previousPivot *types.Header // Pivot from previous sync run (for pivot move detection) phase atomic.Uint32 // Current syncPhase; atomic so phase transitions are visible across goroutines
complete bool // Whether the persisted progress was a completed sync
tasks []*accountTaskV2 // Current account task set being synced tasks []*accountTaskV2 // Current account task set being synced
update chan struct{} // Notification channel for possible sync progression update chan struct{} // Notification channel for possible sync progression
@ -358,11 +403,17 @@ type syncerV2 struct {
storageSynced uint64 // Number of storage slots downloaded storageSynced uint64 // Number of storage slots downloaded
storageBytes common.StorageSize // Number of storage trie bytes persisted to disk 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. extProgress *syncProgressV2 // progress that can be exposed to external caller.
startTime time.Time // Time instance when snapshot sync started startTime time.Time // Time instance when snapshot sync started
logTime time.Time // Time instance when status was last reported 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 pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown
lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, pivot) 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 // newSyncerV2 creates a new snapshot syncer to download the Ethereum state over the
// snap protocol. // snap protocol.
func newSyncerV2(db ethdb.Database, scheme string) *syncerV2 { func newSyncerV2(db ethdb.Database, scheme string) *syncerV2 {
return &syncerV2{ s := &syncerV2{
db: db, db: db,
scheme: scheme, scheme: scheme,
@ -392,7 +443,26 @@ func newSyncerV2(db ethdb.Database, scheme string) *syncerV2 {
accessListReqs: make(map[uint64]*accessListRequest), 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. // 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 // 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 // with the given pivot header and reconstruct the nodes based on the snapshot
// leaves. // leaves.
func (s *syncerV2) Sync(pivot *types.Header, cancel chan struct{}) error { func (s *syncerV2) Sync(target *types.Header, cancel chan struct{}) error {
if pivot == nil { if target == nil {
return errors.New("snap sync: pivot header is nil") return errors.New("snap sync: pivot header is nil")
} }
s.lock.Lock() s.lock.Lock()
s.pivot = pivot
s.previousPivot = nil // loadSyncStatus overwrites when resuming from persisted progress
s.statelessPeers = make(map[string]struct{}) s.statelessPeers = make(map[string]struct{})
s.lock.Unlock() s.lock.Unlock()
if s.startTime.IsZero() { if s.startTime.IsZero() {
s.startTime = time.Now() s.startTime = time.Now()
} }
root := pivot.Root root := target.Root
// Retrieve the previous sync status from DB. If there's no persisted // Retrieve the previous sync status from DB. If there's no persisted
// status, sync is either fresh or already complete. // 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 // isPivotChanged is true when we have prior progress against a different
// pivot. That means we need to roll forward via catchUp, or wipe and // pivot. That means we need to roll forward via catchUp, or wipe and
// restart if the prior pivot was reorged out. // 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. // 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) log.Info("Snap sync already complete for this pivot", "root", root)
return nil return nil
} }
// We're committing to running this sync. Clear the complete flag so a // 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=true // mid-run save (on cancel or error) doesn't persist a stale complete
// status from a prior pivot. // status from a prior pivot. The download remains done, only the trie
s.lock.Lock() // generation must be redone against the new pivot.
s.complete = false if s.getPhase() == phaseComplete {
s.lock.Unlock() s.setPhase(phaseGenerate)
}
defer func() { defer func() {
// Whether sync completed or not, disregard any future packets // 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) log.Debug("Starting snapshot sync cycle", "root", root)
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 // 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, // progress is still usable. If yes, roll forward via BAL catch-up. If not,
// wipe everything and restart fresh. // wipe everything and restart fresh.
if isPivotChanged { switch {
if isPivotReorged(s.db, s.previousPivot, s.pivot) { case isPivotReorged(s.db, prevPivot, target):
log.Warn("Persisted progress unusable, restarting snap sync from scratch", log.Warn("Restarting snap sync from scratch", "oldnumber", prevPivot.Number, "oldHash", prevPivot.Hash())
"number", s.previousPivot.Number, "oldHash", s.previousPivot.Hash())
s.resetSyncState() s.resetSyncState()
} else if err := s.catchUp(cancel); err != nil { 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 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.lock.Lock()
s.previousPivot = s.pivot s.pivot = target
s.lock.Unlock() s.lock.Unlock()
log.Info("Starting state download", "root", root) 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) 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) 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 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) log.Info("Trie generation complete", "root", root)
// Mark sync complete. The deferred saveSyncStatus persists this with // Mark sync complete. The deferred saveSyncStatus persists this so a
// Complete=true so a follow-up Sync call for the same pivot can skip // follow-up Sync call for the same pivot can skip the work entirely.
// the work entirely. s.setPhase(phaseComplete)
s.lock.Lock()
s.complete = true
s.lock.Unlock()
return nil 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 // download runs the bulk flat-state download. It fetches
// account ranges, storage slots, and bytecodes, writing flat state to disk. // account ranges, storage slots, and bytecodes, writing flat state to disk.
func (s *syncerV2) downloadState(cancel chan struct{}) error { func (s *syncerV2) downloadState(cancel chan struct{}) error {
@ -575,6 +699,7 @@ func (s *syncerV2) downloadState(cancel chan struct{}) error {
accountResps = make(chan *accountResponseV2) accountResps = make(chan *accountResponseV2)
storageResps = make(chan *storageResponseV2) storageResps = make(chan *storageResponseV2)
bytecodeResps = make(chan *bytecodeResponseV2) bytecodeResps = make(chan *bytecodeResponseV2)
lastJournal = time.Now()
) )
for { for {
// Remove all completed tasks and terminate if everything's done // 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 { if len(s.tasks) == 0 {
return nil 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 // Assign all the data retrieval tasks to any free peers
s.assignAccountTasks(accountResps, accountReqFails, cancel) s.assignAccountTasks(accountResps, accountReqFails, cancel)
s.assignBytecodeTasks(bytecodeResps, bytecodeReqFails, cancel) s.assignBytecodeTasks(bytecodeResps, bytecodeReqFails, cancel)
@ -591,15 +724,9 @@ func (s *syncerV2) downloadState(cancel chan struct{}) error {
// Update sync progress // Update sync progress
s.lock.Lock() s.lock.Lock()
s.extProgress = &syncProgressV2{ s.refreshProgressLocked()
AccountSynced: s.accountSynced,
AccountBytes: s.accountBytes,
BytecodeSynced: s.bytecodeSynced,
BytecodeBytes: s.bytecodeBytes,
StorageSynced: s.storageSynced,
StorageBytes: s.storageBytes,
}
s.lock.Unlock() s.lock.Unlock()
// Wait for something to happen // Wait for something to happen
select { select {
case <-s.update: case <-s.update:
@ -657,22 +784,47 @@ func isPivotReorged(db ethdb.Database, prev, curr *types.Header) bool {
return canonical != prev.Hash() 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 // 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 // for the gap blocks, verifies them against block headers, and applies the
// diffs to roll flat state forward. // 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() s.lock.RLock()
from := s.previousPivot.Number.Uint64() + 1 from := s.pivot.Number.Uint64() + 1
to := s.pivot.Number.Uint64() to := target.Number.Uint64()
s.lock.RUnlock() s.lock.RUnlock()
log.Info("Starting BAL catch-up", "from", from, "to", to, "blocks", to-from+1) log.Info("Starting BAL catch-up", "from", from, "to", to, "blocks", to-from+1)
// Collect block hashes and headers for the gap range. s.lock.Lock()
s.accessListTotal = to - from + 1
s.accessListSynced = 0
s.refreshProgressLocked()
s.lock.Unlock()
for start := from; start <= to; start += s.catchUpWindow {
select {
case <-cancel:
return ErrCancelled
default:
}
end := start + s.catchUpWindow - 1
if end > to {
end = to
}
// Collect block hashes and headers for this window.
var ( var (
hashes = make([]common.Hash, 0, to-from+1) hashes = make([]common.Hash, 0, end-start+1)
headers = make(map[common.Hash]*types.Header, to-from+1) headers = make(map[common.Hash]*types.Header, end-start+1)
) )
for num := from; num <= to; num++ { for num := start; num <= end; num++ {
hash := rawdb.ReadCanonicalHash(s.db, num) hash := rawdb.ReadCanonicalHash(s.db, num)
if hash == (common.Hash{}) { if hash == (common.Hash{}) {
return fmt.Errorf("missing canonical hash for block %d during catch-up", num) return fmt.Errorf("missing canonical hash for block %d during catch-up", num)
@ -685,7 +837,7 @@ func (s *syncerV2) catchUp(cancel chan struct{}) error {
headers[hash] = header headers[hash] = header
} }
// Fetch BALs from peers // Fetch this window's BALs from peers.
rawBALs, err := s.fetchAccessLists(hashes, headers, cancel) rawBALs, err := s.fetchAccessLists(hashes, headers, cancel)
if err != nil { if err != nil {
return err return err
@ -698,7 +850,7 @@ func (s *syncerV2) catchUp(cancel chan struct{}) error {
return ErrCancelled return ErrCancelled
default: default:
} }
num := from + uint64(i) num := start + uint64(i)
hash := hashes[i] hash := hashes[i]
// Decode the raw RLP into a BAL. // Decode the raw RLP into a BAL.
@ -709,10 +861,6 @@ func (s *syncerV2) catchUp(cancel chan struct{}) error {
if err := rlp.DecodeBytes(raw, &b); err != nil { if err := rlp.DecodeBytes(raw, &b); err != nil {
return fmt.Errorf("failed to decode BAL for block %d: %v", num, err) return fmt.Errorf("failed to decode BAL for block %d: %v", num, err)
} }
// 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 { if err := s.applyAccessList(&b, batch); err != nil {
return fmt.Errorf("BAL application failed for block %d: %v", num, err) return fmt.Errorf("BAL application failed for block %d: %v", num, err)
} }
@ -720,7 +868,7 @@ func (s *syncerV2) catchUp(cancel chan struct{}) error {
// Persist incremental progress so a crash mid-catchUp can resume // Persist incremental progress so a crash mid-catchUp can resume
// from the next unapplied block. // from the next unapplied block.
s.lock.Lock() s.lock.Lock()
s.previousPivot = headers[hash] s.pivot = headers[hash]
s.lock.Unlock() s.lock.Unlock()
s.saveSyncStatusWithDB(batch) s.saveSyncStatusWithDB(batch)
@ -729,7 +877,9 @@ func (s *syncerV2) catchUp(cancel chan struct{}) error {
return err return err
} }
} }
log.Info("BAL catch-up complete", "blocks", len(rawBALs)) log.Info("BAL catch-up progress", "applied", end, "target", to, "remaining", to-end)
}
log.Info("BAL catch-up complete", "from", from, "to", to)
return nil 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)) 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 ( var (
accessListReqFails = make(chan *accessListRequest) accessListReqFails = make(chan *accessListRequest)
accessListResps = make(chan *accessListResponse) accessListResps = make(chan *accessListResponse)
lastStallLog = time.Now() lastStallLog = time.Now()
) )
for len(fetched) < len(hashes) { for len(fetched) < len(hashes) {
// Assign BAL retrieval tasks to idle peers if err := s.checkAccessListProgress(pending, refused); err != nil {
s.assignAccessListTasks(pending, accessListResps, accessListReqFails, cancel) log.Warn("BAL fetch cannot progress", "err", err, "fetched", len(fetched), "remaining", len(pending))
return nil, err
// 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
} }
// Assign BAL retrieval tasks to idle peers
s.assignAccessListTasks(pending, refused, accessListResps, accessListReqFails, cancel)
// Periodic visibility while stalled with peers connected but idle. // Periodic visibility while stalled with peers connected but idle.
if len(pending) > 0 && time.Since(lastStallLog) > 30*time.Second { 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 // A new peer joined, try to assign it work
case id := <-peerDrop: case id := <-peerDrop:
s.revertBALRequests(id, pending) s.revertBALRequests(id, pending)
for h, set := range refused {
delete(set, id)
if len(set) == 0 {
delete(refused, h)
}
}
case <-cancel: case <-cancel:
return nil, ErrCancelled return nil, ErrCancelled
case req := <-accessListReqFails: case req := <-accessListReqFails:
s.revertAccessListRequest(req, pending) s.revertAccessListRequest(req, pending)
case res := <-accessListResps: 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 // Assemble results in input order
results := make([]rlp.RawValue, len(hashes)) 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 // assignAccessListTasks attempts to assign BAL fetch requests to idle
// peers for any hashes still in pending. // peers for any hashes still in pending. Hashes a peer has already refused
func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, success chan *accessListResponse, fail chan *accessListRequest, cancel chan struct{}) { // (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() s.lock.Lock()
defer s.lock.Unlock() 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:] 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 // Generate a unique request ID
var reqid uint64 var reqid uint64
for { for {
@ -834,21 +1019,6 @@ func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, succe
} }
break 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{ req := &accessListRequest{
peer: idle, peer: idle,
id: reqid, id: reqid,
@ -883,7 +1053,7 @@ func (s *syncerV2) assignAccessListTasks(pending map[common.Hash]struct{}, succe
// processAccessListResponse handles a successful BAL response. It // processAccessListResponse handles a successful BAL response. It
// verifies each non-empty BAL against the corresponding block header and // verifies each non-empty BAL against the corresponding block header and
// stores the verified ones in fetched. // 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 ( var (
stateless bool stateless bool
valid = make(map[common.Hash]rlp.RawValue) 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 { for i, raw := range res.accessLists {
h := res.req.hashes[i] 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 bytes.Equal(raw, rlp.EmptyString) {
if refused[h] == nil {
refused[h] = make(map[string]struct{})
}
refused[h][res.req.peer] = struct{}{}
continue continue
} }
var b bal.BlockAccessList 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 // Re-add hashes that were not served back or invalid to pending
for i := 0; i < len(res.req.hashes); i++ { for i := 0; i < len(res.req.hashes); i++ {
if _, ok := valid[res.req.hashes[i]]; ok { if _, ok := valid[res.req.hashes[i]]; ok {
delete(refused, res.req.hashes[i])
continue continue
} }
pending[res.req.hashes[i]] = struct{}{} pending[res.req.hashes[i]] = struct{}{}
@ -952,14 +1129,19 @@ func (s *syncerV2) loadSyncStatus() {
} }
task.StorageCompleted = nil task.StorageCompleted = nil
} }
s.previousPivot = progress.Pivot s.pivot = progress.Pivot
s.complete = progress.Complete s.setPhase(progress.Phase)
s.accountSynced = progress.AccountSynced s.accountSynced = progress.AccountSynced
s.accountBytes = progress.AccountBytes s.accountBytes = progress.AccountBytes
s.bytecodeSynced = progress.BytecodeSynced s.bytecodeSynced = progress.BytecodeSynced
s.bytecodeBytes = progress.BytecodeBytes s.bytecodeBytes = progress.BytecodeBytes
s.storageSynced = progress.StorageSynced s.storageSynced = progress.StorageSynced
s.storageBytes = progress.StorageBytes 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 return
} }
} }
@ -979,13 +1161,20 @@ func increaseKey(key []byte) []byte {
return nil return nil
} }
// DeleteHistoryByRange completely removes all database entries with the specific // deleteRange completely removes all database entries with the specific
// prefix. Note, this method assumes the space with the given prefix is exclusively // prefix. Note, this method assumes the space with the given prefix is
// occupied! // exclusively occupied!
func deleteRange(batch ethdb.Batch, prefix []byte) { func deleteRange(batch ethdb.Batch, prefix []byte) {
start := prefix deleteKeyRange(batch, prefix, increaseKey(bytes.Clone(prefix)))
limit := 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 // Try to remove the data in the range by a loop, as the leveldb
// doesn't support the native range deletion. // doesn't support the native range deletion.
for { for {
@ -997,7 +1186,9 @@ func deleteRange(batch ethdb.Batch, prefix []byte) {
// therefore inconsistent. This is a tradeoff of the current LevelDB-based // therefore inconsistent. This is a tradeoff of the current LevelDB-based
// approach. // approach.
if errors.Is(err, ethdb.ErrTooManyKeys) { 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() batch.Reset()
continue 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 // resetSyncState wipes all persisted snap-sync data (sync status, account
// and storage snapshots) and re-initializes in-memory state with a fresh // and storage snapshots) and re-initializes in-memory state with a fresh
// chunking of the account hash range. // chunking of the account hash range.
@ -1013,17 +1280,21 @@ func (s *syncerV2) resetSyncState() {
rawdb.DeleteSnapshotSyncStatus(batch) rawdb.DeleteSnapshotSyncStatus(batch)
deleteRange(batch, rawdb.SnapshotAccountPrefix) deleteRange(batch, rawdb.SnapshotAccountPrefix)
deleteRange(batch, rawdb.SnapshotStoragePrefix) deleteRange(batch, rawdb.SnapshotStoragePrefix)
s.resetTrienodes(batch)
batch.Write() batch.Write()
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
s.tasks = nil s.tasks = nil
s.previousPivot = nil s.pivot = nil
s.complete = false s.setPhase(phaseDownload)
s.accountSynced, s.accountBytes = 0, 0 s.accountSynced, s.accountBytes = 0, 0
s.bytecodeSynced, s.bytecodeBytes = 0, 0 s.bytecodeSynced, s.bytecodeBytes = 0, 0
s.storageSynced, s.storageBytes = 0, 0 s.storageSynced, s.storageBytes = 0, 0
s.accessListSynced, s.accessListTotal = 0, 0
s.genProgress.Store(0)
s.refreshProgressLocked()
var next common.Hash var next common.Hash
step := new(big.Int).Sub( step := new(big.Int).Sub(
@ -1069,9 +1340,9 @@ func (s *syncerV2) saveSyncStatusWithDB(db ethdb.KeyValueWriter) {
} }
// Store the actual progress markers. // Store the actual progress markers.
progress := &syncProgressV2{ progress := &syncProgressV2{
Pivot: s.previousPivot, Pivot: s.pivot,
Tasks: s.tasks, Tasks: s.tasks,
Complete: s.complete, Phase: s.getPhase(),
AccountSynced: s.accountSynced, AccountSynced: s.accountSynced,
AccountBytes: s.accountBytes, AccountBytes: s.accountBytes,
BytecodeSynced: s.bytecodeSynced, BytecodeSynced: s.bytecodeSynced,
@ -1088,11 +1359,29 @@ func (s *syncerV2) saveSyncStatusWithDB(db ethdb.KeyValueWriter) {
rawdb.WriteSnapshotSyncStatus(db, status) 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. // Progress returns the snap sync status statistics.
func (s *syncerV2) Progress() *syncProgressV2 { func (s *syncerV2) Progress() *syncProgressV2 {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() 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 // 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 // Persist the received account segments. These flat state maybe
// outdated during the sync, but it can be fixed later during the // outdated during the sync, but it can be fixed later during the
// trie rebuild. // trie generation.
oldAccountBytes := s.accountBytes oldAccountBytes := s.accountBytes
batch := ethdb.HookedBatch{ batch := ethdb.HookedBatch{
@ -2554,25 +2843,45 @@ func (s *syncerV2) reportSyncProgressV2(force bool) {
"accounts", accounts, "slots", storage, "codes", bytecode, "eta", common.PrettyDuration(estTime-elapsed)) "accounts", accounts, "slots", storage, "codes", bytecode, "eta", common.PrettyDuration(estTime-elapsed))
} }
// accessListPeersExhausted reports whether forward progress on BAL fetches is // checkAccessListProgress reports whether the BAL fetch can still make
// impossible: at least one peer is connected, every connected peer is marked // forward progress against the current peer set.
// stateless, and no BAL requests are in flight. func (s *syncerV2) checkAccessListProgress(pending map[common.Hash]struct{}, refused map[common.Hash]map[string]struct{}) error {
func (s *syncerV2) accessListPeersExhausted() bool {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
if len(s.peers) == 0 { if len(s.peers) == 0 {
return false return nil
} }
if len(s.accessListReqs) > 0 { if len(s.accessListReqs) > 0 {
return false return nil
} }
serviceable := make(map[string]struct{}, len(s.peers))
for id := range s.peers { for id := range s.peers {
if _, ok := s.statelessPeers[id]; !ok { 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 // sortIdlePeers builds a list of idle peers sorted by download capacity

View file

@ -547,6 +547,65 @@ func testSyncV2(t *testing.T, scheme string) {
verifyAdoptedSyncedState(scheme, syncer.db, sourceAccountTrie.Hash(), elems, t) 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: // verifyAdoptedSyncedState exercises the snap/2 completion contract end-to-end:
// after a real sync, opening a fresh triedb and calling AdoptSyncedState must // after a real sync, opening a fresh triedb and calling AdoptSyncedState must
// (a) succeed and (b) leave flat-state reads serving immediately, with no // (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 // canonical header at block 100 has a different hash. Sync is then called with
// a new pivot at the same height. // a new pivot at the same height.
// //
// If isPivotReorged works, loadSyncStatus restores previousPivot, the check // If isPivotReorged works, loadSyncStatus restores the persisted pivot, the
// flags it as reorged, resetSyncState clears previousPivot, catchUp is // check flags it as reorged, resetSyncState clears it, catchUp is skipped,
// skipped, and the fresh download proceeds to completion. // and the fresh download proceeds to completion.
// //
// If detection doesn't fire, the pivot-move check would call catchUp with // 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 // 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. // and non-zero counter so the reset path has something to clean up.
orphanPivot := mkPivot(100, common.HexToHash("0xdead")) orphanPivot := mkPivot(100, common.HexToHash("0xdead"))
seed := newSyncerV2(db, nodeScheme) seed := newSyncerV2(db, nodeScheme)
// previousPivot reflects where flat state matches and it is what // pivot reflects where flat state matches and it is what saveSyncStatus
// saveSyncStatus persists. Set it to simulate a prior sync reaching // persists. Set it to simulate a prior sync reaching orphanPivot.
// orphanPivot.
seed.previousPivot = orphanPivot
seed.pivot = orphanPivot seed.pivot = orphanPivot
seed.accountSynced = 42 seed.accountSynced = 42
seed.tasks = []*accountTaskV2{{ seed.tasks = []*accountTaskV2{{
Next: common.HexToHash("0x80"), Next: common.HexToHash("0x80"),
Last: common.MaxHash, Last: common.MaxHash,
SubTasks: make(map[common.Hash][]*storageTaskV2), SubTasks: make(map[common.Hash][]*storageTaskV2),
stateCompleted: make(map[common.Hash]struct{}),
}} }}
seed.saveSyncStatus() seed.saveSyncStatus()
@ -1391,14 +1447,14 @@ func TestSyncDetectsPivotReorged(t *testing.T) {
if err := syncer.Sync(newPivot, cancel); err != nil { if err := syncer.Sync(newPivot, cancel); err != nil {
t.Fatalf("sync failed (reorg detection likely broken): %v", err) 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. // against the new (canonical) pivot.
loader := newSyncerV2(db, nodeScheme) loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus() loader.loadSyncStatus()
if !loader.complete { if loader.getPhase() != phaseComplete {
t.Fatal("sync status should be marked Complete=true after successful completion") 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") t.Fatalf("expected persisted pivot to match new pivot")
} }
if data := rawdb.ReadAccountSnapshot(db, orphanAccountHash); len(data) != 0 { if data := rawdb.ReadAccountSnapshot(db, orphanAccountHash); len(data) != 0 {
@ -1445,9 +1501,8 @@ func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
syncer1.Register(src1) syncer1.Register(src1)
src1.remote = syncer1 src1.remote = syncer1
pivot := mkPivot(0, root) pivot := mkPivot(0, root)
syncer1.pivot = pivot
syncer1.previousPivot = pivot // Sync sets this before downloadState
syncer1.loadSyncStatus() syncer1.loadSyncStatus()
syncer1.pivot = pivot // Sync pins this before downloadState
syncer1.downloadState(cancel1) syncer1.downloadState(cancel1)
// Save progress // Save progress
@ -1483,9 +1538,8 @@ func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
syncer2.Register(src2) syncer2.Register(src2)
src2.remote = syncer2 src2.remote = syncer2
pivot2 := mkPivot(0, root) pivot2 := mkPivot(0, root)
syncer2.pivot = pivot2
syncer2.previousPivot = pivot2 // Sync sets this before downloadState
syncer2.loadSyncStatus() syncer2.loadSyncStatus()
syncer2.pivot = pivot2 // Sync pins this before downloadState
if err := syncer2.downloadState(cancel2); err != nil { if err := syncer2.downloadState(cancel2); err != nil {
t.Fatalf("resumed download failed: %v", err) 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 // TestSyncPersistsPivotDuringDownload verifies that after a fresh Sync is
// interrupted mid-download, the persisted previousPivot equals the current // interrupted mid-download, the persisted pivot equals the current pivot
// pivot (not nil). Without this, a follow-up Sync at a different pivot // (not nil). Without this, a follow-up Sync at a different pivot would not
// would not see that the partial flat state belongs to the old pivot, and // see that the partial flat state belongs to the old pivot, and would mix
// would mix old-pivot accounts with new-pivot data. // old-pivot accounts with new-pivot data.
func TestSyncPersistsPivotDuringDownload(t *testing.T) { func TestSyncPersistsPivotDuringDownload(t *testing.T) {
t.Parallel() t.Parallel()
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme) 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. // Sync should be interrupted by the cancel after a couple of responses.
_ = syncer.Sync(pivot, cancel) _ = syncer.Sync(pivot, cancel)
// Persisted previousPivot must equal the pivot, so a follow-up Sync at a // Persisted pivot must equal the pivot, so a follow-up Sync at a different
// different pivot can recognize the partial flat state belongs to this one. // pivot can recognize the partial flat state belongs to this one.
loader := newSyncerV2(db, nodeScheme) loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus() loader.loadSyncStatus()
if loader.previousPivot == nil { if loader.pivot == nil {
t.Fatal("expected persisted previousPivot to be set after interrupted download, got nil") t.Fatal("expected persisted pivot to be set after interrupted download, got nil")
} }
if loader.previousPivot.Hash() != pivot.Hash() { if loader.pivot.Hash() != pivot.Hash() {
t.Errorf("persisted previousPivot mismatch: got %v, want %v", loader.previousPivot.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 // 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, // 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. // so a follow-up Sync can resume from there rather than reapplying everything.
func TestCatchUpPersistsIncrementally(t *testing.T) { func TestCatchUpPersistsIncrementally(t *testing.T) {
@ -1776,7 +1830,7 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
blocks[i] = balBlock{header: header, bal: buf.Bytes()} 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. // flat state covers all accounts.
{ {
var ( var (
@ -1826,22 +1880,157 @@ func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
t.Fatal("expected Sync to fail when applyAccessList hits corrupt flat state") 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. // block (A+2). Without per-iteration saves, it would still be at A.
loader := newSyncerV2(db, nodeScheme) loader := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus() loader.loadSyncStatus()
if loader.previousPivot == nil { if loader.pivot == nil {
t.Fatal("expected persisted previousPivot to be set after partial catchUp") t.Fatal("expected persisted pivot to be set after partial catchUp")
} }
wantHash := blocks[1].header.Hash() wantHash := blocks[1].header.Hash()
if loader.previousPivot.Hash() != wantHash { if loader.pivot.Hash() != wantHash {
t.Errorf("persisted previousPivot mismatch after partial catchUp: got %v, want %v (block A+2)", t.Errorf("persisted pivot mismatch after partial catchUp: got %v, want %v (block A+2)",
loader.previousPivot.Hash(), wantHash) 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 // 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. // subsequent Sync call distinguish "already done" from "fresh node" and skip.
func TestSyncStatusMarkedCompleteAfterCompletion(t *testing.T) { func TestSyncStatusMarkedCompleteAfterCompletion(t *testing.T) {
t.Parallel() t.Parallel()
@ -1870,13 +2059,13 @@ func testSyncStatusMarkedCompleteAfterCompletion(t *testing.T, scheme string) {
} }
// After successful sync, persisted status should be present with // 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 := newSyncerV2(syncer.db, nodeScheme)
loader.loadSyncStatus() loader.loadSyncStatus()
if !loader.complete { if loader.getPhase() != phaseComplete {
t.Fatal("expected persisted status to have Complete=true after successful sync") 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") 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) 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 { if err := syncer.db.DeleteRange(rawdb.SnapshotAccountPrefix, []byte{rawdb.SnapshotAccountPrefix[0] + 1}); err != nil {
t.Fatalf("failed to wipe account snapshot: %v", err) 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 // TestInterruptedGenerationRecovery verifies that if sync is interrupted after
// download completes but before trie rebuild finishes, the next Sync() call // download completes but before trie generation finishes, the next Sync() call
// re-runs the download (which completes immediately) and rebuild. // re-runs the download (which completes immediately) and generation.
func TestInterruptedRebuildRecovery(t *testing.T) { func TestInterruptedGenerationRecovery(t *testing.T) {
t.Parallel() t.Parallel()
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme) nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
root := sourceAccountTrie.Hash() root := sourceAccountTrie.Hash()
// First run: complete download, save status, simulate interruption // First run: complete download, save status, simulate interruption
// before rebuild by calling downloadState() directly // before generation by calling downloadState() directly
var ( var (
once1 sync.Once once1 sync.Once
cancel1 = make(chan struct{}) cancel1 = make(chan struct{})
@ -1946,9 +2135,8 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
syncer1.Register(src1) syncer1.Register(src1)
src1.remote = syncer1 src1.remote = syncer1
pivot := mkPivot(0, root) pivot := mkPivot(0, root)
syncer1.pivot = pivot
syncer1.previousPivot = pivot // Sync sets this before downloadState
syncer1.loadSyncStatus() syncer1.loadSyncStatus()
syncer1.pivot = pivot // Sync pins this before downloadState
if err := syncer1.downloadState(cancel1); err != nil { if err := syncer1.downloadState(cancel1); err != nil {
t.Fatalf("download failed: %v", err) t.Fatalf("download failed: %v", err)
@ -1960,11 +2148,11 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
syncer1.cleanAccountTasks() syncer1.cleanAccountTasks()
syncer1.saveSyncStatus() syncer1.saveSyncStatus()
// Status should exist (rebuild hasn't run yet) // Status should exist (generation hasn't run yet)
if rawdb.ReadSnapshotSyncStatus(db) == nil { if rawdb.ReadSnapshotSyncStatus(db) == nil {
t.Fatal("sync status should exist after download") 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 ( var (
once2 sync.Once once2 sync.Once
cancel2 = make(chan struct{}) cancel2 = make(chan struct{})
@ -1980,14 +2168,199 @@ func TestInterruptedRebuildRecovery(t *testing.T) {
if err := syncer2.Sync(mkPivot(0, root), cancel2); err != nil { if err := syncer2.Sync(mkPivot(0, root), cancel2); err != nil {
t.Fatalf("resumed sync failed: %v", err) 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 := newSyncerV2(db, nodeScheme)
loader.loadSyncStatus() loader.loadSyncStatus()
if !loader.complete { if loader.getPhase() != phaseComplete {
t.Fatal("sync status should be marked Complete=true after rebuild completes") 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 // TestFetchAccessListsMultiplePeers verifies that fetch distributes work
// across multiple idle peers. // across multiple idle peers.
func TestFetchAccessListsMultiplePeers(t *testing.T) { func TestFetchAccessListsMultiplePeers(t *testing.T) {
@ -2462,7 +2835,7 @@ func TestCatchUpRetriesOnBadBAL(t *testing.T) {
// makeStorageTrieFromSlots builds a storage trie for owner from raw slot // makeStorageTrieFromSlots builds a storage trie for owner from raw slot
// key->value pairs, using the exact on-disk encoding the flat snapshot and the // 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 // 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 // no leaf). It returns the storage root, the dirty node set, and the sorted
// snapshot leaves (which a test peer serves verbatim). // 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 // slot, an overwrite of an existing slot, a write of zero (deletion), and a
// multi-tx write where the post-block value wins. // 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 // 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, // 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 // 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. // independent confirmation.
func TestCatchUpAppliesStorageBALs(t *testing.T) { func TestCatchUpAppliesStorageBALs(t *testing.T) {
t.Parallel() 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 // 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 // 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 during catch-up) and the expected post-catch-up state root
// (verified by the trie rebuild). // (verified by the trie generation).
db := rawdb.NewMemoryDatabase() db := rawdb.NewMemoryDatabase()
numA := uint64(128) numA := uint64(128)
emptyH := common.Hash{} emptyH := common.Hash{}
@ -2644,7 +3017,7 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
rawdb.WriteHeader(db, hdrB) rawdb.WriteHeader(db, hdrB)
rawdb.WriteCanonicalHash(db, hdrB.Hash(), numA+1) 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 ( var (
once sync.Once once sync.Once
@ -2665,7 +3038,7 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
} }
close(done) 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. // also confirms the test fixture itself is internally consistent.
verifyTrie(scheme, db, rootA, t) verifyTrie(scheme, db, rootA, t)
@ -2690,6 +3063,12 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
if err := syncer.Sync(hdrB, cancel); err != nil { if err := syncer.Sync(hdrB, cancel); err != nil {
t.Fatalf("pivot A+1 catch-up sync failed: %v", err) 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) close(done)
} }

View file

@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
gasLimit uint64 = 31000 gasLimit uint64 = 31000
startGas uint64 = 10000 startGas uint64 = 10000
value = uint256.NewInt(0) 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) evm.SetTxContext(vmctx.txCtx)
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0}

View file

@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) {
var ( var (
logger = NewStructLogger(nil) logger = NewStructLogger(nil)
evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) 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)} contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)}
var index common.Hash var index common.Hash

View file

@ -839,6 +839,9 @@ type rpcProgress struct {
HealedBytecodeBytes hexutil.Uint64 HealedBytecodeBytes hexutil.Uint64
HealingTrienodes hexutil.Uint64 HealingTrienodes hexutil.Uint64
HealingBytecode hexutil.Uint64 HealingBytecode hexutil.Uint64
SyncedAccessLists hexutil.Uint64
TotalAccessLists hexutil.Uint64
TrieGenProgress hexutil.Uint64
TxIndexFinishedBlocks hexutil.Uint64 TxIndexFinishedBlocks hexutil.Uint64
TxIndexRemainingBlocks hexutil.Uint64 TxIndexRemainingBlocks hexutil.Uint64
StateIndexRemaining hexutil.Uint64 StateIndexRemaining hexutil.Uint64
@ -867,6 +870,9 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
HealedBytecodeBytes: uint64(p.HealedBytecodeBytes), HealedBytecodeBytes: uint64(p.HealedBytecodeBytes),
HealingTrienodes: uint64(p.HealingTrienodes), HealingTrienodes: uint64(p.HealingTrienodes),
HealingBytecode: uint64(p.HealingBytecode), HealingBytecode: uint64(p.HealingBytecode),
SyncedAccessLists: uint64(p.SyncedAccessLists),
TotalAccessLists: uint64(p.TotalAccessLists),
TrieGenProgress: uint64(p.TrieGenProgress),
TxIndexFinishedBlocks: uint64(p.TxIndexFinishedBlocks), TxIndexFinishedBlocks: uint64(p.TxIndexFinishedBlocks),
TxIndexRemainingBlocks: uint64(p.TxIndexRemainingBlocks), TxIndexRemainingBlocks: uint64(p.TxIndexRemainingBlocks),
StateIndexRemaining: uint64(p.StateIndexRemaining), StateIndexRemaining: uint64(p.StateIndexRemaining),

View file

@ -127,14 +127,19 @@ type SyncProgress struct {
SyncedStorage uint64 // Number of storage slots downloaded SyncedStorage uint64 // Number of storage slots downloaded
SyncedStorageBytes uint64 // Number of storage trie bytes persisted to disk SyncedStorageBytes uint64 // Number of storage trie bytes persisted to disk
// Snap/1 specific fields
HealedTrienodes uint64 // Number of state trie nodes downloaded HealedTrienodes uint64 // Number of state trie nodes downloaded
HealedTrienodeBytes uint64 // Number of state trie bytes persisted to disk HealedTrienodeBytes uint64 // Number of state trie bytes persisted to disk
HealedBytecodes uint64 // Number of bytecodes downloaded HealedBytecodes uint64 // Number of bytecodes downloaded
HealedBytecodeBytes uint64 // Number of bytecodes persisted to disk HealedBytecodeBytes uint64 // Number of bytecodes persisted to disk
HealingTrienodes uint64 // Number of state trie nodes pending HealingTrienodes uint64 // Number of state trie nodes pending
HealingBytecode uint64 // Number of bytecodes 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 // "transaction indexing" fields
TxIndexFinishedBlocks uint64 // Number of blocks whose transactions are already indexed TxIndexFinishedBlocks uint64 // Number of blocks whose transactions are already indexed
TxIndexRemainingBlocks uint64 // Number of blocks whose transactions are not indexed yet TxIndexRemainingBlocks uint64 // Number of blocks whose transactions are not indexed yet

View file

@ -182,6 +182,9 @@ func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) {
"healedBytecodeBytes": hexutil.Uint64(progress.HealedBytecodeBytes), "healedBytecodeBytes": hexutil.Uint64(progress.HealedBytecodeBytes),
"healingTrienodes": hexutil.Uint64(progress.HealingTrienodes), "healingTrienodes": hexutil.Uint64(progress.HealingTrienodes),
"healingBytecode": hexutil.Uint64(progress.HealingBytecode), "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), "txIndexFinishedBlocks": hexutil.Uint64(progress.TxIndexFinishedBlocks),
"txIndexRemainingBlocks": hexutil.Uint64(progress.TxIndexRemainingBlocks), "txIndexRemainingBlocks": hexutil.Uint64(progress.TxIndexRemainingBlocks),
"stateIndexRemaining": hexutil.Uint64(progress.StateIndexRemaining), "stateIndexRemaining": hexutil.Uint64(progress.StateIndexRemaining),

View file

@ -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 <http://www.gnu.org/licenses/>.
// 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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
//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::<path>") 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::<path>"
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 ("<id>:memory:<path>" or "<id>:...,memory,...:<path>").
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: "<hierarchy-id>:<comma-separated-controllers>:<path>"
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
//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")
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
//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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -123,7 +123,7 @@ func FormatSlogValue(v slog.Value, tmp []byte) (result []byte) {
var value any var value any
defer func() { defer func() {
if err := recover(); err != nil { 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("<nil>") result = []byte("<nil>")
} else { } else {
panic(err) panic(err)

View file

@ -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 // BuildTestingPayload is for testing_buildBlockV*. It creates a block with the exact content given
// by the parameters instead of using the locally available transactions. // 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{ fullParams := &generateParams{
timestamp: args.Timestamp, timestamp: args.Timestamp,
forceTime: true, forceTime: true,
@ -358,7 +358,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
} }
res := miner.generateWork(context.Background(), fullParams, false) res := miner.generateWork(context.Background(), fullParams, false)
if res.err != nil { 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
} }

View file

@ -250,7 +250,7 @@ func (it *lookupIterator) slowdown() {
if diff > minInterval { if diff > minInterval {
return return
} }
wait := time.NewTimer(diff) wait := time.NewTimer(minInterval - diff)
defer wait.Stop() defer wait.Stop()
select { select {
case <-wait.C: case <-wait.C:

View file

@ -290,28 +290,33 @@ func (tab *Table) refresh() <-chan struct{} {
// preferLive is true and the table contains any verified nodes, the result will not // 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 // contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes. // 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() tab.mutex.Lock()
defer tab.mutex.Unlock() defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many // 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 // buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults). // is O(tab.len() * nresults).
nodes := &nodesByDistance{target: target} if preferLive {
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets { for _, b := range &tab.buckets {
for _, n := range b.entries { for _, n := range b.entries {
if n.isValidatedLive {
nodes.push(n.Node, nresults) nodes.push(n.Node, nresults)
if preferLive && n.isValidatedLive {
liveNodes.push(n.Node, nresults)
} }
} }
} }
if len(nodes.entries) > 0 {
return
}
}
if preferLive && len(liveNodes.entries) > 0 { for _, b := range &tab.buckets {
return liveNodes for _, n := range b.entries {
nodes.push(n.Node, nresults)
} }
return nodes }
return
} }
// appendBucketNodes adds nodes at the given distance to the result slice. // appendBucketNodes adds nodes at the given distance to the result slice.

View file

@ -597,3 +597,77 @@ func newkey() *ecdsa.PrivateKey {
} }
return key 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)
}
})
}
}

View file

@ -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. // RequestENR sends ENRRequest to the given node and waits for a response.
func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) { 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) t.ensureBond(n.ID(), addr)
req := &v4wire.ENRRequest{ req := &v4wire.ENRRequest{

View file

@ -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 type testPacket byte
func (req testPacket) Kind() byte { return byte(req) } func (req testPacket) Kind() byte { return byte(req) }

View file

@ -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. 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. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
Create2Gas uint64 = 32000 // Once per CREATE2 operation 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 CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle
SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation. 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. 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 TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list
TxAccessListStorageKeyGas uint64 = 1900 // Per storage key 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 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 // These have been changed during the course of the chain
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. 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 // the bound has a small safety margin for system-contract accesses that
// don't consume block gas. // don't consume block gas.
BALItemCost uint64 = 2000 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 // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation

View file

@ -166,7 +166,7 @@ func makeDecoder(typ reflect.Type, tags rlpstruct.Tags) (dec decoder, err error)
return decodeU256, nil return decodeU256, nil
case typ == u256Int: case typ == u256Int:
return decodeU256NoPtr, nil return decodeU256NoPtr, nil
case kind == reflect.Ptr: case kind == reflect.Pointer:
return makePtrDecoder(typ, tags) return makePtrDecoder(typ, tags)
case reflect.PointerTo(typ).Implements(decoderInterface): case reflect.PointerTo(typ).Implements(decoderInterface):
return decodeDecoder, nil return decodeDecoder, nil
@ -936,7 +936,7 @@ func (s *Stream) Decode(val interface{}) error {
} }
rval := reflect.ValueOf(val) rval := reflect.ValueOf(val)
rtyp := rval.Type() rtyp := rval.Type()
if rtyp.Kind() != reflect.Ptr { if rtyp.Kind() != reflect.Pointer {
return errNoPointer return errNoPointer
} }
if rval.IsNil() { if rval.IsNil() {

View file

@ -173,7 +173,7 @@ func makeWriter(typ reflect.Type, ts rlpstruct.Tags) (writer, error) {
return writeU256IntPtr, nil return writeU256IntPtr, nil
case typ == u256Int: case typ == u256Int:
return writeU256IntNoPtr, nil return writeU256IntNoPtr, nil
case kind == reflect.Ptr: case kind == reflect.Pointer:
return makePtrWriter(typ, ts) return makePtrWriter(typ, ts)
case reflect.PointerTo(typ).Implements(encoderInterface): case reflect.PointerTo(typ).Implements(encoderInterface):
return makeEncoderWriter(typ), nil return makeEncoderWriter(typ), nil

View file

@ -156,7 +156,7 @@ func parseTag(field Field, lastPublic int) (Tags, error) {
ts.Ignored = true ts.Ignored = true
case "nil", "nilString", "nilList": case "nil", "nilString", "nilList":
ts.NilOK = true 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"} return ts, TagError{Field: name, Tag: t, Err: "field is not a pointer"}
} }
switch t { switch t {

View file

@ -47,7 +47,7 @@ func typeReflectKind(typ types.Type) reflect.Kind {
case *types.Map: case *types.Map:
return reflect.Map return reflect.Map
case *types.Pointer: case *types.Pointer:
return reflect.Ptr return reflect.Pointer
case *types.Signature: case *types.Signature:
return reflect.Func return reflect.Func
case *types.Slice: case *types.Slice:

View file

@ -203,7 +203,7 @@ func rtypeToStructType(typ reflect.Type, rec map[reflect.Type]*rlpstruct.Type) *
IsDecoder: typ.Implements(decoderInterface), IsDecoder: typ.Implements(decoderInterface),
} }
rec[typ] = t 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) t.Elem = rtypeToStructType(typ.Elem(), rec)
} }
return t return t

View file

@ -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 // 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. // 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 { 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) return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
} }
msg, err := c.newMessage(method, args...) msg, err := c.newMessage(method, args...)

View file

@ -407,7 +407,7 @@ func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]
} }
// Set any missing args to nil. // Set any missing args to nil.
for i := len(args); i < len(types); i++ { 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) return nil, fmt.Errorf("missing value for required argument %d", i)
} }
args = append(args, reflect.Zero(types[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 { if err := dec.Decode(argval.Interface()); err != nil {
return args, fmt.Errorf("invalid argument %d: %v", i, err) 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) return args, fmt.Errorf("missing value for required argument %d", i)
} }
args = append(args, argval.Elem()) args = append(args, argval.Elem())

View file

@ -225,7 +225,7 @@ func isErrorType(t reflect.Type) bool {
// Is t Subscription or *Subscription? // Is t Subscription or *Subscription?
func isSubscriptionType(t reflect.Type) bool { func isSubscriptionType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr { for t.Kind() == reflect.Pointer {
t = t.Elem() t = t.Elem()
} }
return t == subscriptionType return t == subscriptionType

View file

@ -346,17 +346,27 @@ func TestTracingSubscribeUnsubscribe(t *testing.T) {
// like notifications (no "id" field). // like notifications (no "id" field).
func postJSONRPC(t *testing.T, url, body string) { func postJSONRPC(t *testing.T, url, body string) {
t.Helper() 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)) req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
if err != nil { if err != nil {
t.Fatalf("new request: %v", err) return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
t.Fatalf("request: %v", err) return err
} }
_, _ = io.Copy(io.Discard, resp.Body) _, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close() resp.Body.Close()
return nil
} }
// TestTracingHTTPNotification verifies that a JSON-RPC notification emits the // 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 // test_block waits on ctx.Done() and returns an error. The internal
// timer cancels ctx, so test_block unblocks shortly after the timeout // timer cancels ctx, so test_block unblocks shortly after the timeout
// response goes out. // 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 // Wait for the in-flight request to finish so the deferred spanEnd fires
// before GetSpans is called. // before GetSpans is called.

View file

@ -325,10 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StartTimer() b.StartTimer()
start := time.Now() start := time.Now()
initialGas := vm.NewGasBudget(msg.GasLimit) initialGas := vm.NewGasBudget(msg.GasLimit, 0)
// Execute the message. // 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 { if err != nil {
b.Error(err) b.Error(err)
return return
@ -337,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.StopTimer() b.StopTimer()
elapsed += uint64(time.Since(start)) elapsed += uint64(time.Since(start))
refund += state.StateDB.GetRefund() refund += state.StateDB.GetRefund()
gasUsed += leftOverGas.Used(initialGas) gasUsed += result.Used(initialGas)
state.StateDB.RevertToSnapshot(snapshot) state.StateDB.RevertToSnapshot(snapshot)
} }

View file

@ -80,8 +80,8 @@ func (tt *TransactionTest) Run() error {
if err != nil { if err != nil {
return return
} }
// Intrinsic gas // Intrinsic cost
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 { if err != nil {
return return
} }

View file

@ -65,6 +65,21 @@ const (
partitionFinished = ^uint64(0) 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. // rangeIterators bundles the per-partition account and storage iterators.
type rangeIterators struct { type rangeIterators struct {
db ethdb.Database 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 // both per-account storage subtries and the partition's slice of the
// account trie. Returns the partition's stripped subtree root blob, or // account trie. Returns the partition's stripped subtree root blob, or
// nil if the partition had no accounts at all. // 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) iters := openRangeIterators(db, rangeStart)
defer iters.release() defer iters.release()
@ -154,6 +169,13 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if len(path) == 1 { if len(path) == 1 {
root = common.CopyBytes(blob) 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) 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 { if bytes.Compare(accountHash[:], rangeEnd[:]) > 0 {
break break
} }
scanned.Add(1) c.accounts.Add(1)
pos.Store(binary.BigEndian.Uint64(accountHash[:8])) c.progress[partition].Store(binary.BigEndian.Uint64(accountHash[:8]))
// Decode the account object // Decode the account object
account, err := types.FullAccount(iters.acct.Value()) 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. // Build the account's storage trie from the flat storage snapshot.
// StackTrie's onTrieNode callback persists nodes as they finalize. // StackTrie's onTrieNode callback persists nodes as they finalize.
storageTrie := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) { 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) 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) copy(lastDanglingAccount, storageAccount)
log.Error("Unexpected storage entries for dangling account", "expected", accountHash, "got", common.BytesToHash(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:] slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]
rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(storageAccount), common.BytesToHash(slotHash)) rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(storageAccount), common.BytesToHash(slotHash))
if err := iters.flushIfFull(batch, "dangling"); err != nil { 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 { if err := storageTrie.Update(slotHash, iters.stor.Value()); err != nil {
return nil, fmt.Errorf("storage stack trie update for %x: %w", accountHash, err) 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 { if err := iters.flushIfFull(batch, "storage"); err != nil {
return nil, err return nil, err
} }
@ -252,7 +282,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
if computed != account.Root { if computed != account.Root {
account.Root = computed account.Root = computed
rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(*account)) rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(*account))
updated.Add(1) c.accountUpdated.Add(1)
} }
fullAccount, err := rlp.EncodeToBytes(account) fullAccount, err := rlp.EncodeToBytes(account)
if err != nil { if err != nil {
@ -291,7 +321,7 @@ func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Dat
copy(lastDanglingTail, acct) copy(lastDanglingTail, acct)
log.Error("Unexpected storage entries for dangling account", "addrhash", common.BytesToHash(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:] slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]
rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(acct), common.BytesToHash(slotHash)) rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(acct), common.BytesToHash(slotHash))
if err := iters.flushIfFull(batch, "dangling tail"); err != nil { if err := iters.flushIfFull(batch, "dangling tail"); err != nil {
@ -337,7 +367,7 @@ func hashRanges(total int) [][2]common.Hash {
return ranges 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 // data in the database. The account hash space is partitioned into 16
// slices aligned with the first-nibble branching of the MPT root. Each // slices aligned with the first-nibble branching of the MPT root. Each
// partition is processed by its own goroutine, which walks its slice, // 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 // trie. Once every partition has produced its subtree root, the top-level
// branch is assembled and its hash verified against the expected root. // branch is assembled and its hash verified against the expected root.
// //
// Resume: on entry, any partition that has a "done" marker from a // Generation is all or nothing: an interrupted run leaves no resume
// previous run is skipped. Its subtree blob is read from the marker // state and the next run builds every partition from scratch.
// and handed to assembleRoot directly. On a mid-run crash, only the
// in-flight partition(s) are redone.
func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-chan struct{}) (GenerateStats, error) { 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 ( var (
start = time.Now() start = time.Now()
scanned atomic.Int64 c genCounters
updated atomic.Int64
deleted atomic.Int64
progress [numPartitions]atomic.Uint64
progressDone = make(chan struct{}) progressDone = make(chan struct{})
// partitionBlobs[i] holds the root node for partition i, or nil if // partitionBlobs[i] holds the root node for partition i, or nil if
// the partition is empty. // the partition is empty.
partitionBlobs [numPartitions][]byte partitionBlobs [numPartitions][]byte
) )
go tickProgress(progressDone, start, &scanned, &updated, &progress) go tickProgress(progressDone, start, &c, prog)
defer close(progressDone) defer close(progressDone)
// For each partition, either skip (prior done marker found) or run // Run every partition concurrently, each producing the subtree root
// it. Prior runs can leave the partition's raw root blob in the done // blob that assembleRoot needs.
// marker. We recover it here so assembleRoot has everything it needs.
var ( var (
ranges = hashRanges(numPartitions) ranges = hashRanges(numPartitions)
eg, ctx = errgroup.WithContext(context.Background()) 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 { for i, r := range ranges {
partition := byte(i) partition := byte(i)
rangeStart, rangeEnd := r[0], r[1] 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 { eg.Go(func() error {
start := time.Now() 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 { if err != nil {
return err return err
} }
log.Info("Partition done", "partition", partition, "elapsed", common.PrettyDuration(time.Since(start))) log.Info("Partition done", "partition", partition, "elapsed", common.PrettyDuration(time.Since(start)))
progress[partition].Store(partitionFinished) c.progress[partition].Store(partitionFinished)
partitionBlobs[partition] = blob 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 return nil
}) })
} }
@ -404,10 +423,11 @@ func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-c
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {
return GenerateStats{}, err return GenerateStats{}, err
} }
if prog != nil {
// Assemble the top-level root from the partition blobs, verify it prog.Store(100)
// matches the expected root, and clear all partition markers on }
// success. // Assemble the top-level root from the partition blobs and verify it
// matches the expected root.
got, err := assembleRoot(db, scheme, partitionBlobs) got, err := assembleRoot(db, scheme, partitionBlobs)
if err != nil { if err != nil {
return GenerateStats{}, fmt.Errorf("assemble root: %w", err) 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 { if got != root {
return GenerateStats{}, fmt.Errorf("state root mismatch: got %x, want %x", 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{ return GenerateStats{
Scanned: scanned.Load(), Scanned: c.accounts.Load(),
Updated: updated.Load(), Updated: c.accountUpdated.Load(),
Deleted: deleted.Load(), Deleted: c.storageDeleted.Load(),
}, nil }, 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 // 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. // 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) ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-done: case <-done:
return return
case <-ticker.C: case <-ticker.C:
elapsed := time.Since(start) 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" eta := "n/a"
if fraction > 0.005 { if fraction > 0.005 {
eta = common.PrettyDuration(time.Duration(float64(elapsed) * (1.0/fraction - 1.0))).String() eta = common.PrettyDuration(time.Duration(float64(elapsed) * (1.0/fraction - 1.0))).String()
} }
log.Info("Generating trie", log.Info("Generating trie",
"progress", fmt.Sprintf("%.1f%%", fraction*100), "eta", eta, "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), "elapsed", common.PrettyDuration(elapsed),
"acct/s", uint64(float64(scanned.Load())/elapsed.Seconds())) "acct/s", uint64(float64(c.accounts.Load())/elapsed.Seconds()))
} }
} }
} }

View file

@ -21,7 +21,6 @@ import (
"context" "context"
"math/big" "math/big"
"sort" "sort"
"sync/atomic"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "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 // TestHashRanges checks that hashRanges fully and contiguously covers the
// 256-bit hash space, with the last range absorbing the rounding remainder. // 256-bit hash space, with the last range absorbing the rounding remainder.
func TestHashRanges(t *testing.T) { func TestHashRanges(t *testing.T) {
@ -765,22 +673,21 @@ func TestGenerateTrieBatchFlush(t *testing.T) {
tc.build(db) tc.build(db)
peak := 0 peak := 0
var scanned, updated, deleted atomic.Int64 var c genCounters
var pos atomic.Uint64
ranges := hashRanges(numPartitions) ranges := hashRanges(numPartitions)
if _, err := generatePartition(context.Background(), nil, peakBatchDB{Database: db, peak: &peak}, 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) t.Fatalf("generatePartition: %v", err)
} }
if scanned.Load() != tc.wantScanned { if c.accounts.Load() != tc.wantScanned {
t.Errorf("scanned = %d, want %d (an account was skipped?)", scanned.Load(), tc.wantScanned) t.Errorf("scanned = %d, want %d (an account was skipped?)", c.accounts.Load(), tc.wantScanned)
} }
if deleted.Load() != tc.wantDeleted { if c.storageDeleted.Load() != tc.wantDeleted {
t.Errorf("deleted = %d, want %d", deleted.Load(), tc.wantDeleted) t.Errorf("deleted = %d, want %d", c.storageDeleted.Load(), tc.wantDeleted)
} }
if updated.Load() != 0 { if c.accountUpdated.Load() != 0 {
t.Errorf("updated = %d, want 0 (a storage slot was dropped across a flush?)", updated.Load()) 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 // The batch must have stayed bounded. Without this site's flush its
// full write set (far larger than IdealBatchSize) buffers into one batch. // full write set (far larger than IdealBatchSize) buffers into one batch.

View file

@ -766,7 +766,7 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
if err == nil { if err == nil {
version = fmt.Sprintf("%d", m.Version) 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 // newHistoryIndexer constructs the history indexer and launches the background

Some files were not shown because too many files have changed in this diff Show more