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