diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index d3f1c4940f..71f169ba93 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -735,7 +735,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM // about the transaction and calling mechanisms. evmContext := core.NewEVMBlockContext(header, b.blockchain, nil) vmEnv := vm.NewEVM(evmContext, stateDB, b.config, vm.Config{NoBaseFee: true}) - gasPool := new(core.GasPool).AddGas(gomath.MaxUint64) + gasPool := core.NewGasPool(gomath.MaxUint64) return core.ApplyMessage(vmEnv, msg, gasPool) } diff --git a/build/ci.go b/build/ci.go index 858a9fb253..41af36b1c1 100644 --- a/build/ci.go +++ b/build/ci.go @@ -107,17 +107,21 @@ var ( Tags: "ziren", Env: map[string]string{"GOMIPS": "softfloat", "CGO_ENABLED": "0"}, }, + { + Name: "womir", + GOOS: "wasip1", + GOARCH: "wasm", + Tags: "womir", + }, { Name: "wasm-js", GOOS: "js", GOARCH: "wasm", - Tags: "example", }, { Name: "wasm-wasi", GOOS: "wasip1", GOARCH: "wasm", - Tags: "example", }, { Name: "example", @@ -163,11 +167,11 @@ var ( // Distros for which packages are created debDistros = []string{ - "xenial", // 16.04, EOL: 04/2026 - "bionic", // 18.04, EOL: 04/2028 - "focal", // 20.04, EOL: 04/2030 - "jammy", // 22.04, EOL: 04/2032 - "noble", // 24.04, EOL: 04/2034 + "xenial", // 16.04, EOL: 04/2026 + "bionic", // 18.04, EOL: 04/2028 + "focal", // 20.04, EOL: 04/2030 + "jammy", // 22.04, EOL: 04/2032 + "noble", // 24.04, EOL: 04/2034 } // This is where the tests should be unpacked. @@ -305,7 +309,7 @@ func doInstallKeeper(cmdline []string) { args := slices.Clone(gobuild.Args) args = append(args, "-o", executablePath(outputName)) args = append(args, ".") - build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env}) + build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env, Dir: gobuild.Dir}) } } @@ -1203,7 +1207,7 @@ func doWindowsInstaller(cmdline []string) { var ( arch = flag.String("arch", runtime.GOARCH, "Architecture for cross build packaging") signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. WINDOWS_SIGNING_KEY)`) - signify = flag.String("signify key", "", `Environment variable holding the signify signing key (e.g. WINDOWS_SIGNIFY_KEY)`) + signify = flag.String("signify", "", `Environment variable holding the signify signing key (e.g. WINDOWS_SIGNIFY_KEY)`) upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`) ) diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index e214cd89cb..74ecc9ffb5 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -235,12 +235,16 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet { return &readError{err} } - n, fromAddr, err := c.ReadFrom(buf) + n, _, err := c.ReadFrom(buf) if err != nil { return &readError{err} } - _, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String()) + // Always use tc.remoteAddr for session lookup. The actual source address of + // the packet may differ from tc.remoteAddr when the remote node is reachable + // via multiple networks (e.g. Docker bridge vs. overlay), but the codec's + // session cache is keyed by the address used during Encode. + _, _, p, err := tc.codec.Decode(buf[:n], tc.remoteAddr.String()) if err != nil { return &readError{err} } diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 0f1c00e03b..cbf0dac55f 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -154,16 +154,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, isEIP4762 = chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number))) statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762) signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) - gaspool = new(core.GasPool) + gaspool = core.NewGasPool(pre.Env.GasLimit) blockHash = common.Hash{0x13, 0x37} rejectedTxs []*rejectedTx includedTxs types.Transactions - gasUsed = uint64(0) blobGasUsed = uint64(0) receipts = make(types.Receipts, 0) ) - - gaspool.AddGas(pre.Env.GasLimit) vmContext := vm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, @@ -265,15 +262,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, statedb.SetTxContext(tx.Hash(), len(receipts)) var ( snapshot = statedb.Snapshot() - prevGas = gaspool.Gas() + gp = gaspool.Snapshot() ) - receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, &gasUsed, evm) + receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()}) - gaspool.SetGas(prevGas) + gaspool.Set(gp) continue } if receipt.Logs == nil { @@ -364,7 +361,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, Receipts: receipts, Rejected: rejectedTxs, Difficulty: (*math.HexOrDecimal256)(vmContext.Difficulty), - GasUsed: (math.HexOrDecimal64)(gasUsed), + GasUsed: (math.HexOrDecimal64)(gaspool.Used()), BaseFee: (*math.HexOrDecimal256)(vmContext.BaseFee), } @@ -380,10 +377,6 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, // Set requestsHash on block. h := types.CalcRequestsHash(requests) execRs.RequestsHash = &h - for i := range requests { - // remove prefix - requests[i] = requests[i][1:] - } execRs.Requests = requests } diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index b94f41d6a2..2bfcff6768 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -29,7 +29,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/tests" @@ -194,8 +196,10 @@ func Transaction(ctx *cli.Context) error { r.Error = errors.New("gas * maxFeePerGas exceeds 256 bits") } // Check whether the init code size has been exceeded. - if chainConfig.IsShanghai(new(big.Int)) && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { - r.Error = errors.New("max initcode size exceeded") + if tx.To() == nil { + if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(tx.Data()))); err != nil { + r.Error = err + } } // Bor: EIP-7825 at Madhugiri HF block diff --git a/cmd/fetchpayload/main.go b/cmd/fetchpayload/main.go new file mode 100644 index 0000000000..eafc05fbe8 --- /dev/null +++ b/cmd/fetchpayload/main.go @@ -0,0 +1,177 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +// fetchpayload queries an Ethereum node over RPC, fetches a block and its +// execution witness, and writes the combined Payload (ChainID + Block + +// Witness) to disk in the format consumed by cmd/keeper. +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" +) + +// Payload is duplicated from cmd/keeper/main.go (package main, not importable). +type Payload struct { + ChainID uint64 + Block *types.Block + Witness *stateless.Witness +} + +func main() { + var ( + rpcURL = flag.String("rpc", "http://localhost:8545", "RPC endpoint URL") + blockArg = flag.String("block", "latest", `Block number: decimal, 0x-hex, or "latest"`) + format = flag.String("format", "rlp", "Comma-separated output formats: rlp, hex, json") + outDir = flag.String("out", "", "Output directory (default: current directory)") + ) + flag.Parse() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Parse block number (nil means "latest" in ethclient). + blockNum, err := parseBlockNumber(*blockArg) + if err != nil { + fatal("invalid block number %q: %v", *blockArg, err) + } + + // Connect to the node. + client, err := ethclient.DialContext(ctx, *rpcURL) + if err != nil { + fatal("failed to connect to %s: %v", *rpcURL, err) + } + defer client.Close() + + chainID, err := client.ChainID(ctx) + if err != nil { + fatal("failed to get chain ID: %v", err) + } + + // Fetch the block first so we have a concrete number for the witness call, + // avoiding a race where "latest" advances between the two RPCs. + block, err := client.BlockByNumber(ctx, blockNum) + if err != nil { + fatal("failed to fetch block: %v", err) + } + fmt.Printf("Fetched block %d (%#x)\n", block.NumberU64(), block.Hash()) + + // Fetch the execution witness via the debug namespace. + var extWitness stateless.ExtWitness + err = client.Client().CallContext(ctx, &extWitness, "debug_executionWitness", rpc.BlockNumber(block.NumberU64())) + if err != nil { + fatal("failed to fetch execution witness: %v", err) + } + + witness := new(stateless.Witness) + err = witness.FromExtWitness(&extWitness) + if err != nil { + fatal("failed to convert witness: %v", err) + } + + payload := Payload{ + ChainID: chainID.Uint64(), + Block: block, + Witness: witness, + } + + // Encode payload as RLP (shared by "rlp" and "hex" formats). + rlpBytes, err := rlp.EncodeToBytes(payload) + if err != nil { + fatal("failed to RLP-encode payload: %v", err) + } + + // Write one output file per requested format. + blockHex := fmt.Sprintf("%x", block.NumberU64()) + for f := range strings.SplitSeq(*format, ",") { + f = strings.TrimSpace(f) + outPath := filepath.Join(*outDir, fmt.Sprintf("%s_payload.%s", blockHex, f)) + + var data []byte + switch f { + case "rlp": + data = rlpBytes + case "hex": + data = []byte(hexutil.Encode(rlpBytes)) + case "json": + data, err = marshalJSONPayload(chainID, block, &extWitness) + if err != nil { + fatal("failed to JSON-encode payload: %v", err) + } + default: + fatal("unknown format %q (valid: rlp, hex, json)", f) + } + + if err := os.WriteFile(outPath, data, 0644); err != nil { + fatal("failed to write %s: %v", outPath, err) + } + fmt.Printf("Wrote %s (%d bytes)\n", outPath, len(data)) + } +} + +// parseBlockNumber converts a CLI string to *big.Int. +// Returns nil for "latest" (ethclient convention for the head block). +func parseBlockNumber(s string) (*big.Int, error) { + if strings.EqualFold(s, "latest") { + return nil, nil + } + n := new(big.Int) + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + if _, ok := n.SetString(s[2:], 16); !ok { + return nil, fmt.Errorf("invalid hex number") + } + return n, nil + } + if _, ok := n.SetString(s, 10); !ok { + return nil, fmt.Errorf("invalid decimal number") + } + return n, nil +} + +// jsonPayload is a JSON-friendly representation of Payload. It uses ExtWitness +// instead of the internal Witness (which has no JSON marshaling). +type jsonPayload struct { + ChainID uint64 `json:"chainId"` + Block *types.Block `json:"block"` + Witness *stateless.ExtWitness `json:"witness"` +} + +func marshalJSONPayload(chainID *big.Int, block *types.Block, ext *stateless.ExtWitness) ([]byte, error) { + return json.MarshalIndent(jsonPayload{ + ChainID: chainID.Uint64(), + Block: block, + Witness: ext, + }, "", " ") +} + +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index c15f23a98b..08a187d812 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -206,13 +206,19 @@ This command dumps out the state for a given block (or latest, if none provided) pruneHistoryCommand = &cli.Command{ Action: pruneHistory, Name: "prune-history", - Usage: "Prune blockchain history (block bodies and receipts) up to the merge block", + Usage: "Prune blockchain history (block bodies and receipts) up to a specified point", ArgsUsage: "", - Flags: utils.DatabaseFlags, + Flags: slices.Concat(utils.DatabaseFlags, []cli.Flag{ + utils.ChainHistoryFlag, + }), Description: ` The prune-history command removes historical block bodies and receipts from the -blockchain database up to the merge block, while preserving block headers. This -helps reduce storage requirements for nodes that don't need full historical data.`, +blockchain database up to a specified point, while preserving block headers. This +helps reduce storage requirements for nodes that don't need full historical data. + +The --history.chain flag is required to specify the pruning target: + - postmerge: Prune up to the merge block. The node will keep the merge block and everything thereafter. + - postprague: Prune up to the Prague (Pectra) upgrade block. The node will keep the prague block and everything thereafter.`, } downloadEraCommand = &cli.Command{ @@ -695,47 +701,74 @@ func hashish(x string) bool { } func pruneHistory(ctx *cli.Context) error { + // Parse and validate the history mode flag. + if !ctx.IsSet(utils.ChainHistoryFlag.Name) { + return errors.New("--history.chain flag is required") + } + var mode history.HistoryMode + if err := mode.UnmarshalText([]byte(ctx.String(utils.ChainHistoryFlag.Name))); err != nil { + return err + } + if mode == history.KeepAll { + return errors.New("--history.chain=all is not valid for pruning. To restore history, use 'geth import-history'") + } + stack, _ := makeConfigNode(ctx) defer stack.Close() - // Open the chain database + // Open the chain database. chain, chaindb := utils.MakeChain(ctx, stack, false) defer chaindb.Close() defer chain.Stop() - // Determine the prune point. This will be the first PoS block. - prunePoint, ok := history.PrunePoints[chain.Genesis().Hash()] - if !ok || prunePoint == nil { - return errors.New("prune point not found") + // Determine the prune point based on the history mode. + genesisHash := chain.Genesis().Hash() + prunePoint := history.GetPrunePoint(genesisHash, mode) + if prunePoint == nil { + return fmt.Errorf("prune point for %q not found for this network", mode.String()) } var ( - mergeBlock = prunePoint.BlockNumber - mergeBlockHash = prunePoint.BlockHash.Hex() + targetBlock = prunePoint.BlockNumber + targetBlockHash = prunePoint.BlockHash ) - // Check we're far enough past merge to ensure all data is in freezer + // Check the current freezer tail to see if pruning is needed/possible. + freezerTail, _ := chaindb.Tail() + if freezerTail > 0 { + if freezerTail == targetBlock { + log.Info("Database already pruned to target block", "tail", freezerTail) + return nil + } + if freezerTail > targetBlock { + // Database is pruned beyond the target - can't unprune. + return fmt.Errorf("database is already pruned to block %d, which is beyond target %d. Cannot unprune. To restore history, use 'geth import-history'", freezerTail, targetBlock) + } + // freezerTail < targetBlock: we can prune further, continue below. + } + + // Check we're far enough past the target to ensure all data is in freezer. currentHeader := chain.CurrentHeader() if currentHeader == nil { return errors.New("current header not found") } - if currentHeader.Number.Uint64() < mergeBlock+params.FullImmutabilityThreshold { - return fmt.Errorf("chain not far enough past merge block, need %d more blocks", - mergeBlock+params.FullImmutabilityThreshold-currentHeader.Number.Uint64()) + if currentHeader.Number.Uint64() < targetBlock+params.FullImmutabilityThreshold { + return fmt.Errorf("chain not far enough past target block %d, need %d more blocks", + targetBlock, targetBlock+params.FullImmutabilityThreshold-currentHeader.Number.Uint64()) } - // Double-check the prune block in db has the expected hash. - hash := rawdb.ReadCanonicalHash(chaindb, mergeBlock) - if hash != common.HexToHash(mergeBlockHash) { - return fmt.Errorf("merge block hash mismatch: got %s, want %s", hash.Hex(), mergeBlockHash) + // Double-check the target block in db has the expected hash. + hash := rawdb.ReadCanonicalHash(chaindb, targetBlock) + if hash != targetBlockHash { + return fmt.Errorf("target block hash mismatch: got %s, want %s", hash.Hex(), targetBlockHash.Hex()) } - log.Info("Starting history pruning", "head", currentHeader.Number, "tail", mergeBlock, "tailHash", mergeBlockHash) + log.Info("Starting history pruning", "head", currentHeader.Number, "target", targetBlock, "targetHash", targetBlockHash.Hex()) start := time.Now() - rawdb.PruneTransactionIndex(chaindb, mergeBlock) - if _, err := chaindb.TruncateTail(mergeBlock); err != nil { + rawdb.PruneTransactionIndex(chaindb, targetBlock) + if _, err := chaindb.TruncateTail(targetBlock); err != nil { return fmt.Errorf("failed to truncate ancient data: %v", err) } - log.Info("History pruning completed", "tail", mergeBlock, "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("History pruning completed", "tail", targetBlock, "elapsed", common.PrettyDuration(time.Since(start))) // TODO(s1na): what if there is a crash between the two prune operations? diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 9120897b3b..4a69bf0eb2 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -54,7 +54,7 @@ var ( } removeChainDataFlag = &cli.BoolFlag{ Name: "remove.chain", - Usage: "If set, selects the state data for removal", + Usage: "If set, selects the chain data for removal", } inspectTrieTopFlag = &cli.IntFlag{ Name: "top", diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 45769a52a3..87129706d4 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -24,7 +24,6 @@ import ( "os/signal" "slices" "sort" - "strconv" "syscall" "time" @@ -326,20 +325,6 @@ func prepare(ctx *cli.Context) { case !ctx.IsSet(utils.NetworkIdFlag.Name): log.Info("Starting Geth on Ethereum mainnet...") } - // If we're a full node on mainnet without --cache specified, bump default cache allowance - if !ctx.IsSet(utils.CacheFlag.Name) && !ctx.IsSet(utils.NetworkIdFlag.Name) { - // Make sure we're not on any supported preconfigured testnet either - if !ctx.IsSet(utils.SepoliaFlag.Name) && - !ctx.IsSet(utils.MumbaiFlag.Name) && - !ctx.IsSet(utils.HoleskyFlag.Name) && - !ctx.IsSet(utils.AmoyFlag.Name) && - !ctx.IsSet(utils.HoodiFlag.Name) && - !ctx.IsSet(utils.DeveloperFlag.Name) { - // Nope, we're really on mainnet. Bump that cache up! - log.Info("Bumping default cache on mainnet", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 4096) - _ = ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096)) - } - } } // geth is the main entry point into the system if no special subcommand is run. diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index faf2b578da..ee31b3bb81 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -25,8 +25,6 @@ import ( "slices" "time" - "github.com/urfave/cli/v2" - "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" @@ -38,6 +36,8 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/triedb" + "github.com/urfave/cli/v2" ) var ( @@ -106,7 +106,9 @@ information about the specified address. Usage: "Traverse the state with given root hash and perform quick verification", ArgsUsage: "", Action: traverseState, - Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags), + Flags: slices.Concat([]cli.Flag{ + utils.AccountFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), Description: ` geth snapshot traverse-state will traverse the whole state from the given state root and will abort if any @@ -114,6 +116,8 @@ referenced trie node or contract code is missing. This command can be used for state integrity verification. The default checking target is the HEAD state. It's also usable without snapshot enabled. + +If --account is specified, only the storage trie of that account is traversed. `, }, { @@ -121,7 +125,9 @@ It's also usable without snapshot enabled. Usage: "Traverse the state with given root hash and perform detailed verification", ArgsUsage: "", Action: traverseRawState, - Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags), + Flags: slices.Concat([]cli.Flag{ + utils.AccountFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), Description: ` geth snapshot traverse-rawstate will traverse the whole state from the given root and will abort if any referenced @@ -130,6 +136,8 @@ verification. The default checking target is the HEAD state. It's basically iden to traverse-state, but the check granularity is smaller. It's also usable without snapshot enabled. + +If --account is specified, only the storage trie of that account is traversed. `, }, { @@ -182,18 +190,15 @@ func pruneState(ctx *cli.Context) error { Datadir: stack.ResolvePath(""), BloomSize: ctx.Uint64(utils.BloomFilterSizeFlag.Name), } - pruner, err := pruner.NewPruner(chaindb, prunerconfig) if err != nil { log.Error("Failed to open snapshot tree", "err", err) return err } - if ctx.NArg() > 1 { log.Error("Too many arguments given") return errors.New("too many arguments") } - var targetRoot common.Hash if ctx.NArg() == 1 { targetRoot, err = parseRoot(ctx.Args().First()) @@ -202,12 +207,10 @@ func pruneState(ctx *cli.Context) error { return err } } - if err = pruner.Prune(targetRoot); err != nil { log.Error("Failed to prune state", "err", err) return err } - return nil } @@ -217,6 +220,7 @@ func verifyState(ctx *cli.Context) error { chaindb := utils.MakeChainDatabase(ctx, stack, true, false) defer chaindb.Close() + headBlock := rawdb.ReadHeadBlock(chaindb) if headBlock == nil { log.Error("Failed to load head block") @@ -277,6 +281,120 @@ func checkDanglingStorage(ctx *cli.Context) error { return snapshot.CheckDanglingStorage(db) } +// parseAccount parses the account flag value as either an address (20 bytes) +// or an account hash (32 bytes) and returns the hashed account key. +func parseAccount(input string) (common.Hash, error) { + switch len(input) { + case 40, 42: // address + return crypto.Keccak256Hash(common.HexToAddress(input).Bytes()), nil + case 64, 66: // hash + return common.HexToHash(input), nil + default: + return common.Hash{}, errors.New("malformed account address or hash") + } +} + +// lookupAccount resolves the account from the state trie using the given +// account hash. +func lookupAccount(accountHash common.Hash, tr *trie.Trie) (*types.StateAccount, error) { + accData, err := tr.Get(accountHash.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to get account %s: %w", accountHash, err) + } + if accData == nil { + return nil, fmt.Errorf("account not found: %s", accountHash) + } + var acc types.StateAccount + if err := rlp.DecodeBytes(accData, &acc); err != nil { + return nil, fmt.Errorf("invalid account data %s: %w", accountHash, err) + } + return &acc, nil +} + +func traverseStorage(id *trie.ID, db *triedb.Database, report bool, detail bool) error { + tr, err := trie.NewStateTrie(id, db) + if err != nil { + log.Error("Failed to open storage trie", "account", id.Owner, "root", id.Root, "err", err) + return err + } + var ( + slots int + nodes int + lastReport time.Time + start = time.Now() + ) + it, err := tr.NodeIterator(nil) + if err != nil { + log.Error("Failed to open storage iterator", "account", id.Owner, "root", id.Root, "err", err) + return err + } + logger := log.Debug + if report { + logger = log.Info + } + logger("Start traversing storage trie", "account", id.Owner, "storageRoot", id.Root) + + if !detail { + iter := trie.NewIterator(it) + for iter.Next() { + slots += 1 + if time.Since(lastReport) > time.Second*8 { + logger("Traversing storage", "account", id.Owner, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start))) + lastReport = time.Now() + } + } + if iter.Err != nil { + log.Error("Failed to traverse storage trie", "root", id.Root, "err", iter.Err) + return iter.Err + } + logger("Storage is complete", "account", id.Owner, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start))) + } else { + reader, err := db.NodeReader(id.StateRoot) + if err != nil { + log.Error("Failed to open state reader", "err", err) + return err + } + var ( + buffer = make([]byte, 32) + hasher = crypto.NewKeccakState() + ) + for it.Next(true) { + nodes += 1 + node := it.Hash() + + // Check the presence for non-empty hash node(embedded node doesn't + // have their own hash). + if node != (common.Hash{}) { + blob, _ := reader.Node(id.Owner, it.Path(), node) + if len(blob) == 0 { + log.Error("Missing trie node(storage)", "hash", node) + return errors.New("missing storage") + } + hasher.Reset() + _, _ = hasher.Write(blob) + hasher.Read(buffer) + if !bytes.Equal(buffer, node.Bytes()) { + log.Error("Invalid trie node(storage)", "hash", node.Hex(), "value", blob) + return errors.New("invalid storage node") + } + } + if it.Leaf() { + slots += 1 + } + if time.Since(lastReport) > time.Second*8 { + logger("Traversing storage", "account", id.Owner, "nodes", nodes, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start))) + lastReport = time.Now() + } + } + if err := it.Error(); err != nil { + log.Error("Failed to traverse storage trie", "root", id.Root, "err", err) + return err + } + logger("Storage is complete", "account", id.Owner, "nodes", nodes, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start))) + } + return nil +} + // traverseState is a helper function used for pruning verification. // Basically it just iterates the trie, ensure all nodes and associated // contract codes are present. @@ -295,35 +413,54 @@ func traverseState(ctx *cli.Context) error { log.Error("Failed to load head block") return errors.New("no head block") } - if ctx.NArg() > 1 { log.Error("Too many arguments given") return errors.New("too many arguments") } - var ( root common.Hash err error ) - if ctx.NArg() == 1 { root, err = parseRoot(ctx.Args().First()) if err != nil { log.Error("Failed to resolve state root", "err", err) return err } - log.Info("Start traversing the state", "root", root) } else { root = headBlock.Root() log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64()) } + // If --account is specified, only traverse the storage trie of that account. + if accountStr := ctx.String(utils.AccountFlag.Name); accountStr != "" { + accountHash, err := parseAccount(accountStr) + if err != nil { + log.Error("Failed to parse account", "err", err) + return err + } + // Use raw trie since the account key is already hashed. + t, err := trie.New(trie.StateTrieID(root), triedb) + if err != nil { + log.Error("Failed to open state trie", "root", root, "err", err) + return err + } + acc, err := lookupAccount(accountHash, t) + if err != nil { + log.Error("Failed to look up account", "hash", accountHash, "err", err) + return err + } + if acc.Root == types.EmptyRootHash { + log.Info("Account has no storage", "hash", accountHash) + return nil + } + return traverseStorage(trie.StorageTrieID(root, accountHash, acc.Root), triedb, true, false) + } t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb) if err != nil { log.Error("Failed to open trie", "root", root, "err", err) return err } - var ( accounts int slots int @@ -339,64 +476,34 @@ func traverseState(ctx *cli.Context) error { accIter := trie.NewIterator(acctIt) for accIter.Next() { accounts += 1 - var acc types.StateAccount if err := rlp.DecodeBytes(accIter.Value, &acc); err != nil { log.Error("Invalid account encountered during traversal", "err", err) return err } - if acc.Root != types.EmptyRootHash { - id := trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root) - - storageTrie, err := trie.NewStateTrie(id, triedb) + err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root), triedb, false, false) if err != nil { - log.Error("Failed to open storage trie", "root", acc.Root, "err", err) return err } - storageIt, err := storageTrie.NodeIterator(nil) - if err != nil { - log.Error("Failed to open storage iterator", "root", acc.Root, "err", err) - return err - } - storageIter := trie.NewIterator(storageIt) - for storageIter.Next() { - slots += 1 - - if time.Since(lastReport) > time.Second*8 { - log.Info("Traversing state", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) - lastReport = time.Now() - } - } - - if storageIter.Err != nil { - log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Err) - return storageIter.Err - } } - if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) { log.Error("Code is missing", "hash", common.BytesToHash(acc.CodeHash)) return errors.New("missing code") } - codes += 1 } - if time.Since(lastReport) > time.Second*8 { log.Info("Traversing state", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) lastReport = time.Now() } } - if accIter.Err != nil { log.Error("Failed to traverse state trie", "root", root, "err", accIter.Err) return accIter.Err } - log.Info("State is complete", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) - return nil } @@ -419,35 +526,54 @@ func traverseRawState(ctx *cli.Context) error { log.Error("Failed to load head block") return errors.New("no head block") } - if ctx.NArg() > 1 { log.Error("Too many arguments given") return errors.New("too many arguments") } - var ( root common.Hash err error ) - if ctx.NArg() == 1 { root, err = parseRoot(ctx.Args().First()) if err != nil { log.Error("Failed to resolve state root", "err", err) return err } - log.Info("Start traversing the state", "root", root) } else { root = headBlock.Root() log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64()) } + // If --account is specified, only traverse the storage trie of that account. + if accountStr := ctx.String(utils.AccountFlag.Name); accountStr != "" { + accountHash, err := parseAccount(accountStr) + if err != nil { + log.Error("Failed to parse account", "err", err) + return err + } + // Use raw trie since the account key is already hashed. + t, err := trie.New(trie.StateTrieID(root), triedb) + if err != nil { + log.Error("Failed to open state trie", "root", root, "err", err) + return err + } + acc, err := lookupAccount(accountHash, t) + if err != nil { + log.Error("Failed to look up account", "hash", accountHash, "err", err) + return err + } + if acc.Root == types.EmptyRootHash { + log.Info("Account has no storage", "hash", accountHash) + return nil + } + return traverseStorage(trie.StorageTrieID(root, accountHash, acc.Root), triedb, true, true) + } t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb) if err != nil { log.Error("Failed to open trie", "root", root, "err", err) return err } - var ( nodes int accounts int @@ -480,11 +606,9 @@ func traverseRawState(ctx *cli.Context) error { log.Error("Missing trie node(account)", "hash", node) return errors.New("missing account") } - hasher.Reset() _, _ = hasher.Write(blob) _, _ = hasher.Read(got) - if !bytes.Equal(got, node.Bytes()) { log.Error("Invalid trie node(account)", "hash", node.Hex(), "value", blob) return errors.New("invalid account node") @@ -494,87 +618,35 @@ func traverseRawState(ctx *cli.Context) error { // dig into the storage trie further. if accIter.Leaf() { accounts += 1 - var acc types.StateAccount if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil { log.Error("Invalid account encountered during traversal", "err", err) return errors.New("invalid account") } - if acc.Root != types.EmptyRootHash { - id := trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root) - - storageTrie, err := trie.NewStateTrie(id, triedb) - if err != nil { - log.Error("Failed to open storage trie", "root", acc.Root, "err", err) - return errors.New("missing storage trie") - } - storageIter, err := storageTrie.NodeIterator(nil) + err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root), triedb, false, true) if err != nil { - log.Error("Failed to open storage iterator", "root", acc.Root, "err", err) return err } - for storageIter.Next(true) { - nodes += 1 - node := storageIter.Hash() - - // Check the presence for non-empty hash node(embedded node doesn't - // have their own hash). - if node != (common.Hash{}) { - blob, _ := reader.Node(common.BytesToHash(accIter.LeafKey()), storageIter.Path(), node) - if len(blob) == 0 { - log.Error("Missing trie node(storage)", "hash", node) - return errors.New("missing storage") - } - - hasher.Reset() - _, _ = hasher.Write(blob) - _, _ = hasher.Read(got) - - if !bytes.Equal(got, node.Bytes()) { - log.Error("Invalid trie node(storage)", "hash", node.Hex(), "value", blob) - return errors.New("invalid storage node") - } - } - // Bump the counter if it's leaf node. - if storageIter.Leaf() { - slots += 1 - } - if time.Since(lastReport) > time.Second*8 { - log.Info("Traversing state", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) - lastReport = time.Now() - } - } - - if storageIter.Error() != nil { - log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Error()) - return storageIter.Error() - } } - if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) { log.Error("Code is missing", "account", common.BytesToHash(accIter.LeafKey())) return errors.New("missing code") } - codes += 1 } - if time.Since(lastReport) > time.Second*8 { log.Info("Traversing state", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) lastReport = time.Now() } } } - if accIter.Error() != nil { log.Error("Failed to traverse state trie", "root", root, "err", accIter.Error()) return accIter.Error() } - log.Info("State is complete", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) - return nil } @@ -583,7 +655,6 @@ func parseRoot(input string) (common.Hash, error) { if err := h.UnmarshalText([]byte(input)); err != nil { return h, err } - return h, nil } @@ -605,33 +676,27 @@ func dumpState(ctx *cli.Context) error { if err != nil { return err } - accIt, err := stateIt.AccountIterator(root, common.BytesToHash(conf.Start)) if err != nil { return err } - defer accIt.Release() log.Info("Snapshot dumping started", "root", root) - var ( start = time.Now() logged = time.Now() accounts uint64 ) - enc := json.NewEncoder(os.Stdout) enc.Encode(struct { Root common.Hash `json:"root"` }{root}) - for accIt.Next() { account, err := types.FullAccount(accIt.Account()) if err != nil { return err } - da := &state.DumpAccount{ Balance: account.Balance.String(), Nonce: account.Nonce, @@ -639,11 +704,9 @@ func dumpState(ctx *cli.Context) error { CodeHash: account.CodeHash, AddressHash: accIt.Hash().Bytes(), } - if !conf.SkipCode && !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) { da.Code = rawdb.ReadCode(db, common.BytesToHash(account.CodeHash)) } - if !conf.SkipStorage { da.Storage = make(map[common.Hash]string) @@ -651,29 +714,23 @@ func dumpState(ctx *cli.Context) error { if err != nil { return err } - for stIt.Next() { da.Storage[stIt.Hash()] = common.Bytes2Hex(stIt.Slot()) } } - enc.Encode(da) - accounts++ if time.Since(logged) > 8*time.Second { log.Info("Snapshot dumping in progress", "at", accIt.Hash(), "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start))) - logged = time.Now() } - if conf.Max > 0 && accounts >= conf.Max { break } } log.Info("Snapshot dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start))) - return nil } @@ -719,12 +776,10 @@ func checkAccount(ctx *cli.Context) error { if ctx.NArg() != 1 { return errors.New("need arg") } - var ( hash common.Hash addr common.Address ) - switch arg := ctx.Args().First(); len(arg) { case 40, 42: addr = common.HexToAddress(arg) @@ -734,22 +789,15 @@ func checkAccount(ctx *cli.Context) error { default: return errors.New("malformed address or hash") } - stack, _ := makeConfigNode(ctx) defer stack.Close() - chaindb := utils.MakeChainDatabase(ctx, stack, true, false) defer chaindb.Close() - start := time.Now() - log.Info("Checking difflayer journal", "address", addr, "hash", hash) - if err := snapshot.CheckJournalAccount(chaindb, hash); err != nil { return err } - log.Info("Checked the snapshot journalled storage", "time", common.PrettyDuration(time.Since(start))) - return nil } diff --git a/cmd/keeper/getpayload_example.go b/cmd/keeper/getpayload_example.go index 683cc79248..8f40a7bd11 100644 --- a/cmd/keeper/getpayload_example.go +++ b/cmd/keeper/getpayload_example.go @@ -15,6 +15,7 @@ // along with the go-ethereum library. If not, see . //go:build example +// +build example package main diff --git a/cmd/keeper/getpayload_wasm.go b/cmd/keeper/getpayload_wasm.go index b912678825..5024ac7d49 100644 --- a/cmd/keeper/getpayload_wasm.go +++ b/cmd/keeper/getpayload_wasm.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build wasm -// +build wasm +//go:build wasm && !womir +// +build wasm,!womir package main diff --git a/cmd/keeper/getpayload_womir.go b/cmd/keeper/getpayload_womir.go new file mode 100644 index 0000000000..8645dc7c26 --- /dev/null +++ b/cmd/keeper/getpayload_womir.go @@ -0,0 +1,49 @@ +// 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 womir + +package main + +import "unsafe" + +// These match the WOMIR guest-io imports (env module). +// Protocol: __hint_input prepares next item, __hint_buffer reads words. +// Each item has format: [byte_len_u32_le, ...data_words_padded_to_4bytes] +// +//go:wasmimport env __hint_input +func hintInput() + +//go:wasmimport env __hint_buffer +func hintBuffer(ptr unsafe.Pointer, numWords uint32) +func readWord() uint32 { + var buf [4]byte + hintBuffer(unsafe.Pointer(&buf[0]), 1) + return uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24 +} +func readBytes() []byte { + hintInput() + byteLen := readWord() + numWords := (byteLen + 3) / 4 + data := make([]byte, numWords*4) + hintBuffer(unsafe.Pointer(&data[0]), numWords) + return data[:byteLen] +} + +// getInput reads the RLP-encoded Payload from the WOMIR hint stream. +func getInput() []byte { + return readBytes() +} diff --git a/cmd/keeper/stubs.go b/cmd/keeper/stubs.go index 407a21a145..de7ee64353 100644 --- a/cmd/keeper/stubs.go +++ b/cmd/keeper/stubs.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !example && !ziren && !wasm -// +build !example,!ziren,!wasm +//go:build !example && !ziren && !wasm && !womir +// +build !example,!ziren,!wasm,!womir package main diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 4e3688eb35..8422c93c52 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -233,17 +233,15 @@ var ( Usage: "Max number of elements (0 = no limit)", Value: 0, } - TopFlag = &cli.IntFlag{ - Name: "top", - Usage: "Print the top N results", - Value: 5, + AccountFlag = &cli.StringFlag{ + Name: "account", + Usage: "Specifies the account address or hash to traverse a single storage trie", } OutputFileFlag = &cli.StringFlag{ Name: "output", Usage: "Writes the result in json to the output", Value: "", } - SnapshotFlag = &cli.BoolFlag{ Name: "snapshot", Usage: `Enables snapshot-database mode (default = enable)`, @@ -340,7 +338,7 @@ var ( } ChainHistoryFlag = &cli.StringFlag{ Name: "history.chain", - Usage: `Blockchain history retention ("all" or "postmerge")`, + Usage: `Blockchain history retention ("all", "postmerge", or "postprague")`, Value: ethconfig.Defaults.HistoryMode.String(), Category: flags.StateCategory, } @@ -510,8 +508,8 @@ var ( // Performance tuning settings CacheFlag = &cli.IntFlag{ Name: "cache", - Usage: "Megabytes of memory allocated to internal caching (default = 4096 mainnet full node, 128 light mode)", - Value: 1024, + Usage: "Megabytes of memory allocated to internal caching", + Value: 4096, Category: flags.PerfCategory, } CacheDatabaseFlag = &cli.IntFlag{ diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 52d8f53839..818aab3be7 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1684,7 +1684,7 @@ func (c *Bor) checkAndCommitSpan( tempState := state.Inner().Copy() tempState.ResetPrefetcher() - tempState.StartPrefetcher("bor", state.Witness(), nil) + tempState.StartPrefetcher("bor", state.Witness()) span, err := c.spanner.GetCurrentSpan(ctx, header.ParentHash, tempState) if err != nil { @@ -1829,7 +1829,7 @@ func (c *Bor) CommitStates( // Fetch the LastStateId from contract via current state instance tempState := state.Inner().Copy() tempState.ResetPrefetcher() - tempState.StartPrefetcher("bor", state.Witness(), nil) + tempState.StartPrefetcher("bor", state.Witness()) lastStateIDBig, err = c.GenesisContractsClient.LastStateId(tempState, number-1, header.ParentHash) if err != nil { diff --git a/console/console.go b/console/console.go index 27d42be3fc..eb0ce24d37 100644 --- a/console/console.go +++ b/console/console.go @@ -305,7 +305,7 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str for ; start > 0; start-- { // Skip all methods and namespaces (i.e. including the dot) c := line[start] - if c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '1' && c <= '9') { + if c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { continue } // We've hit an unexpected character, autocomplete form here diff --git a/core/blockchain.go b/core/blockchain.go index 412f10b1eb..e7da9d4e74 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -887,7 +887,7 @@ func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header, wit go func() { pstart := time.Now() - parallelStatedb.StartPrefetcher("chain", witness, nil) + parallelStatedb.StartPrefetcher("chain", witness) v2VmCfg := bc.cfg.VmConfig sharedCaches.applyTo(&v2VmCfg) res, err := bc.parallelProcessor.Process(block, parallelStatedb, v2VmCfg, nil, ctx) @@ -919,7 +919,7 @@ func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header, wit go func() { pstart := time.Now() - statedb.StartPrefetcher("chain", witness, nil) + statedb.StartPrefetcher("chain", witness) res, err := bc.processor.Process(block, statedb, bc.cfg.VmConfig, nil, ctx) blockExecutionSerialTimer.UpdateSince(pstart) var localVtime time.Duration @@ -1159,8 +1159,12 @@ func (bc *BlockChain) loadLastState() error { // initializeHistoryPruning sets bc.historyPrunePoint. func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { - freezerTail, _ := bc.db.Tail() - + var ( + freezerTail, _ = bc.db.Tail() + genesisHash = bc.genesisBlock.Hash() + mergePoint = history.MergePrunePoints[genesisHash] + praguePoint = history.PraguePrunePoints[genesisHash] + ) switch bc.cfg.ChainHistoryMode { case history.KeepAll: if freezerTail == 0 { @@ -1168,34 +1172,66 @@ func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { } // The database was pruned somehow, so we need to figure out if it's a known // configuration or an error. - predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()] - if predefinedPoint == nil || freezerTail != predefinedPoint.BlockNumber { - log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail) - return errors.New("unexpected database tail") + if mergePoint != nil && freezerTail == mergePoint.BlockNumber { + bc.historyPrunePoint.Store(mergePoint) + return nil } - bc.historyPrunePoint.Store(predefinedPoint) - return nil + if praguePoint != nil && freezerTail == praguePoint.BlockNumber { + bc.historyPrunePoint.Store(praguePoint) + return nil + } + log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail) + return errors.New("unexpected database tail") // nolint:staticcheck case history.KeepPostMerge: + if mergePoint == nil { + return errors.New("history pruning requested for unknown network") + } if freezerTail == 0 && latest != 0 { - // This is the case where a user is trying to run with --history.chain - // postmerge directly on an existing DB. We could just trigger the pruning - // here, but it'd be a bit dangerous since they may not have intended this - // action to happen. So just tell them how to do it. log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String())) - log.Error(fmt.Sprintf("Run 'geth prune-history' to prune pre-merge history.")) + log.Error("Run 'geth prune-history --history.chain postmerge' to prune pre-merge history.") return errors.New("history pruning requested via configuration") } - predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()] - if predefinedPoint == nil { - log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash()) + // Check if DB is pruned further than requested (to Prague). + if praguePoint != nil && freezerTail == praguePoint.BlockNumber { + log.Error("Chain history database is pruned to Prague block, but postmerge mode was requested.") + log.Error("History cannot be unpruned. To restore history, use 'geth import-history'.") + log.Error("If you intended to keep post-Prague history, use '--history.chain postprague' instead.") + return errors.New("database pruned beyond requested history mode") + } + if freezerTail > 0 && freezerTail != mergePoint.BlockNumber { + return errors.New("chain history database pruned to unknown block") + } + bc.historyPrunePoint.Store(mergePoint) + return nil + + case history.KeepPostPrague: + if praguePoint == nil { return errors.New("history pruning requested for unknown network") - } else if freezerTail > 0 && freezerTail != predefinedPoint.BlockNumber { + } + // Check if already at the prague prune point. + if freezerTail == praguePoint.BlockNumber { + bc.historyPrunePoint.Store(praguePoint) + return nil + } + // Check if database needs pruning. + if latest != 0 { + if freezerTail == 0 { + log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String())) + log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.") + return errors.New("history pruning requested via configuration") + } + if mergePoint != nil && freezerTail == mergePoint.BlockNumber { + log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is only pruned to merge block.", bc.cfg.ChainHistoryMode.String())) + log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.") + return errors.New("history pruning requested via configuration") + } log.Error("Chain history database is pruned to unknown block", "tail", freezerTail) return errors.New("unexpected database tail") } - bc.historyPrunePoint.Store(predefinedPoint) + // Fresh database (latest == 0), will sync from prague point. + bc.historyPrunePoint.Store(praguePoint) return nil default: @@ -1733,6 +1769,8 @@ func (bc *BlockChain) ExportN(w io.Writer, first uint64, last uint64) error { func (bc *BlockChain) writeHeadBlock(block *types.Block) { // Add the block to the canonical chain number scheme and mark as the head batch := bc.db.NewBatch() + defer batch.Close() + rawdb.WriteHeadHeaderHash(batch, block.Hash()) rawdb.WriteHeadFastBlockHash(batch, block.Hash()) rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64()) @@ -2373,6 +2411,8 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // Note all the components of block(td, hash->number map, header, body, receipts) // should be written atomically. BlockBatch is used for containing all components. blockBatch := bc.db.NewBatch() + defer blockBatch.Close() + rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd) rawdb.WriteBlock(blockBatch, block) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) @@ -3310,7 +3350,7 @@ func (bc *BlockChain) insertChainWithWitnesses(chain types.Blocks, setHead bool, // only block being inserted. A bit crude, but witnesses are huge, // so we refuse to make an entire chain of them. if bc.cfg.VmConfig.StatelessSelfValidation || (makeWitness && len(chain) == 1) { - witness, err = stateless.NewWitness(block.Header(), bc) + witness, err = stateless.NewWitness(block.Header(), bc, bc.cfg.VmConfig.EnableWitnessStats) if err != nil { return nil, it.index, err } @@ -3350,7 +3390,7 @@ func (bc *BlockChain) insertChainWithWitnesses(chain types.Blocks, setHead bool, } if computeWitness { - witness, err = stateless.NewWitness(block.Header(), bc) + witness, err = stateless.NewWitness(block.Header(), bc, bc.cfg.VmConfig.EnableWitnessStats) if err != nil { log.Error("Error in witness generation", "err", err) } @@ -3557,7 +3597,6 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s vtime := time.Since(vstart) var witness *stateless.Witness - var witnessStats *stateless.WitnessStats // If witnesses was generated and stateless self-validation requested, do // that now. Self validation should *never* run in production, it's more of @@ -3568,10 +3607,6 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s if witness = statedb.Witness(); witness != nil && bc.cfg.VmConfig.StatelessSelfValidation { log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash()) - if bc.cfg.VmConfig.EnableWitnessStats { - witnessStats = stateless.NewWitnessStats() - } - // Remove critical computed fields from the block to force true recalculation context := block.Header() context.Root = common.Hash{} @@ -3629,8 +3664,8 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s return nil, err } // Report the collected witness statistics - if witnessStats != nil { - witnessStats.ReportMetrics(block.NumberU64()) + if witness != nil { + witness.ReportMetrics(block.NumberU64()) } // Update the metrics touched during block commit @@ -3996,6 +4031,7 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // as the txlookups should be changed atomically, and all subsequent // reads should be blocked until the mutation is complete. bc.txLookupLock.Lock() + defer bc.txLookupLock.Unlock() // Reorg can be executed, start reducing the chain's old blocks and appending // the new blocks @@ -4077,6 +4113,8 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // Delete useless indexes right now which includes the non-canonical // transaction indexes, canonical chain indexes which above the head. batch := bc.db.NewBatch() + defer batch.Close() + for _, tx := range types.HashDifference(deletedTxs, rebirthTxs) { rawdb.DeleteTxLookupEntry(batch, tx) } @@ -4100,9 +4138,6 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // Reset the tx lookup cache to clear stale txlookup cache. bc.txLookupCache.Purge() - // Release the tx-lookup lock after mutation. - bc.txLookupLock.Unlock() - return nil } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 0a2cd47a9f..9dffc29806 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -5565,7 +5565,7 @@ func TestStatelessInsertChain(t *testing.T) { witnessesParallel := make([]*stateless.Witness, len(blocksParallel)) for i, b := range blocksParallel { - w, err := stateless.NewWitness(b.Header(), chain) + w, err := stateless.NewWitness(b.Header(), chain, false) if err != nil { t.Fatalf("failed to build witness for block %d: %v", b.NumberU64(), err) } @@ -5597,7 +5597,7 @@ func TestStatelessInsertChain(t *testing.T) { // Now import via InsertChainStateless and expect clean happy path in sequential witnessesSequential := make([]*stateless.Witness, len(blocksSequential)) for i, b := range blocksSequential { - w, err := stateless.NewWitness(b.Header(), chain) + w, err := stateless.NewWitness(b.Header(), chain, false) if err != nil { t.Fatalf("failed to build witness for block %d: %v", b.NumberU64(), err) } diff --git a/core/chain_makers.go b/core/chain_makers.go index 370feb1e03..51eac3c144 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -66,7 +66,7 @@ func (b *BlockGen) SetCoinbase(addr common.Address) { } b.header.Coinbase = addr - b.gasPool = new(GasPool).AddGas(b.header.GasLimit) + b.gasPool = NewGasPool(b.header.GasLimit) } // SetExtra sets the extra data field of the generated block. @@ -120,10 +120,12 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed) + receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } + b.header.GasUsed = b.gasPool.Used() + // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. if b.statedb.Database().TrieDB().IsVerkle() { @@ -494,14 +496,16 @@ func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int, if genesis.Config != nil && genesis.Config.IsVerkle(genesis.Config.ChainID) { triedbConfig = triedb.VerkleDefaults } - triedb := triedb.NewDatabase(db, triedbConfig) - defer triedb.Close() - _, err := genesis.Commit(db, triedb) + genesisTriedb := triedb.NewDatabase(db, triedbConfig) + block, err := genesis.Commit(db, genesisTriedb) if err != nil { + genesisTriedb.Close() panic(err) } - blocks, receipts := GenerateChain(genesis.Config, genesis.ToBlock(), engine, db, n, gen) + genesisTriedb.Close() + + blocks, receipts := GenerateChain(genesis.Config, block, engine, db, n, gen) return db, blocks, receipts } diff --git a/core/error.go b/core/error.go index 5dc8c14c45..04a6ade915 100644 --- a/core/error.go +++ b/core/error.go @@ -84,14 +84,14 @@ var ( // by a transaction is higher than what's left in the block. ErrGasLimitReached = errors.New("gas limit reached") + // ErrGasLimitOverflow is returned by the gas pool if the remaining gas + // exceeds the maximum value of uint64. + ErrGasLimitOverflow = errors.New("gas limit overflow") + // ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't // have enough funds for transfer(topmost call only). ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") - // ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger - // than init code size limit. - ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded") - // ErrInsufficientBalanceWitness is returned if the transaction sender has enough // funds to cover the transfer, but not enough to pay for witness access/modification // costs for the transaction diff --git a/core/eth_transfer_logs_test.go b/core/eth_transfer_logs_test.go new file mode 100644 index 0000000000..9e204f691a --- /dev/null +++ b/core/eth_transfer_logs_test.go @@ -0,0 +1,180 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "encoding/binary" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +var ethTransferTestCode = common.FromHex("6080604052600436106100345760003560e01c8063574ffc311461003957806366e41cb714610090578063f8a8fd6d1461009a575b600080fd5b34801561004557600080fd5b5061004e6100a4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100986100ac565b005b6100a26100f5565b005b63deadbeef81565b7f38e80b5c85ba49b7280ccc8f22548faa62ae30d5a008a1b168fba5f47f5d1ee560405160405180910390a1631234567873ffffffffffffffffffffffffffffffffffffffff16ff5b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405160405180910390a163deadbeef73ffffffffffffffffffffffffffffffffffffffff166002348161014657fe5b046040516024016040516020818303038152906040527f66e41cb7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106101fd57805182526020820191506020810190506020830392506101da565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461025f576040519150601f19603f3d011682016040523d82523d6000602084013e610264565b606091505b50505056fea265627a7a723158202cce817a434785d8560c200762f972d453ccd30694481be7545f9035a512826364736f6c63430005100032") + +/* +pragma solidity >=0.4.22 <0.6.0; + +contract TestLogs { + + address public constant target_contract = 0x00000000000000000000000000000000DeaDBeef; + address payable constant selfdestruct_addr = 0x0000000000000000000000000000000012345678; + + event Response(bool success, bytes data); + event TestEvent(); + event TestEvent2(); + + function test() public payable { + emit TestEvent(); + target_contract.call.value(msg.value/2)(abi.encodeWithSignature("test2()")); + } + function test2() public payable { + emit TestEvent2(); + selfdestruct(selfdestruct_addr); + } +} +*/ + +// TestEthTransferLogs tests EIP-7708 ETH transfer log output by simulating a +// scenario including transaction, CALL and SELFDESTRUCT value transfers, and +// also "ordinary" logs emitted. The same scenario is also tested with no value +// transferred. +func TestEthTransferLogs(t *testing.T) { + testEthTransferLogs(t, 1_000_000_000) + testEthTransferLogs(t, 0) +} + +func testEthTransferLogs(t *testing.T, value uint64) { + var ( + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + addr2 = common.HexToAddress("cafebabe") // caller + addr3 = common.HexToAddress("deadbeef") // callee + addr4 = common.HexToAddress("12345678") // selfdestruct target + testEvent = crypto.Keccak256Hash([]byte("TestEvent()")) + testEvent2 = crypto.Keccak256Hash([]byte("TestEvent2()")) + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + ) + + // Amsterdam is wired dormant on Bor and gated by block number rather than + // upstream's timestamp, so activate it from genesis for this test. + config.AmsterdamBlock = new(big.Int) + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + addr1: {Balance: newGwei(1000000000)}, + addr2: {Code: ethTransferTestCode}, + addr3: {Code: ethTransferTestCode}, + }, + } + _, blocks, receipts := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx := types.MustSignNewTx(key1, signer, &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + To: &addr2, + Gas: 500_000, + GasFeeCap: newGwei(5), + GasTipCap: newGwei(5), + Value: big.NewInt(int64(value)), + Data: common.FromHex("f8a8fd6d"), + }) + b.AddTx(tx) + }) + + blockHash := blocks[0].Hash() + txHash := blocks[0].Transactions()[0].Hash() + addr2hash := func(addr common.Address) (hash common.Hash) { + copy(hash[12:], addr[:]) + return + } + u256 := func(amount uint64) []byte { + data := make([]byte, 32) + binary.BigEndian.PutUint64(data[24:], amount) + return data + } + + var expLogs = []*types.Log{ + { + Address: params.SystemAddress, + Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr1), addr2hash(addr2)}, + Data: u256(value), + }, + { + Address: addr2, + Topics: []common.Hash{testEvent}, + Data: nil, + }, + { + Address: params.SystemAddress, + Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr2), addr2hash(addr3)}, + Data: u256(value / 2), + }, + { + Address: addr3, + Topics: []common.Hash{testEvent2}, + Data: nil, + }, + { + Address: params.SystemAddress, + Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr3), addr2hash(addr4)}, + Data: u256(value / 2), + }, + } + // Bor emits its own LogTransfer from the 0x1010 fee address after every + // core.Transfer, plus a LogFeeTransfer for the gas payment, so the EIP-7708 + // logs are interleaved rather than contiguous. Index records where each one + // lands in the full receipt, which pins the interleaving. + indices := []uint{0, 2, 3, 5, 6} + if value == 0 { + // no ETH transfer logs expected with zero value, and Bor's own transfer + // logs are skipped for zero amounts too + expLogs = []*types.Log{expLogs[1], expLogs[3]} + indices = []uint{0, 1} + } + for i, log := range expLogs { + log.BlockNumber = 1 + log.BlockHash = blockHash + log.BlockTimestamp = 10 + log.TxIndex = 0 + log.TxHash = txHash + log.Index = indices[i] + } + + var gotLogs []*types.Log + for _, log := range receipts[0][0].Logs { + if log.Address != feeAddress { + gotLogs = append(gotLogs, log) + } + } + if len(expLogs) != len(gotLogs) { + t.Fatalf("Incorrect number of logs (expected: %d, got: %d)", len(expLogs), len(gotLogs)) + } + for i, log := range gotLogs { + if !reflect.DeepEqual(expLogs[i], log) { + t.Fatalf("Incorrect log at index %d (expected: %v, got: %v)", i, expLogs[i], log) + } + } +} diff --git a/core/evm.go b/core/evm.go index 0efd9ba02c..6247a2f7d9 100644 --- a/core/evm.go +++ b/core/evm.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" ) // ChainContext supports retrieving headers and consensus parameters from the @@ -117,9 +118,10 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common // EthereumTransfer subtracts amount from sender and adds it to recipient, // matching upstream go-ethereum semantics — no Bor transfer-log emission. // Used by NewEVMBlockContext when ChainConfig.Bor is nil. -func EthereumTransfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) { +func EthereumTransfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) { db.SubBalance(sender, amount, tracing.BalanceChangeTransfer) db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer) + emitEthTransferLog(db, sender, recipient, amount, rules) } // NewEVMTxContext creates a new transaction context for a single transaction. @@ -194,7 +196,7 @@ func CanTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool { } // Transfer subtracts amount from sender and adds amount to recipient using the given Db -func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) { +func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) { // In V2 BlockSTM, ParallelStateDB.RecordTransfer returns true and captures // the transfer for log generation during settlement. The serial StateDB // returns false, falling through to the original snapshot-based log path. @@ -203,6 +205,8 @@ func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.I if db.RecordTransfer(sender, recipient, amount) { db.SubBalance(sender, amount, tracing.BalanceChangeTransfer) db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer) + emitEthTransferLog(db, sender, recipient, amount, rules) + return } @@ -212,9 +216,20 @@ func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.I db.SubBalance(sender, amount, tracing.BalanceChangeTransfer) db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer) + emitEthTransferLog(db, sender, recipient, amount, rules) output1 := db.GetBalance(sender) output2 := db.GetBalance(recipient) AddTransferLog(db, sender, recipient, amount.ToBig(), input1.ToBig(), input2.ToBig(), output1.ToBig(), output2.ToBig()) } + +// emitEthTransferLog emits the EIP-7708 transfer log. Called at the same point +// relative to the balance change on every transfer path so that V1 and V2 +// execution agree on log order. On the Bor path this lands alongside the 0x1010 +// LogTransfer, so once Amsterdam activates a value transfer produces both. +func emitEthTransferLog(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) { + if rules.IsAmsterdam && !amount.IsZero() && sender != recipient { + db.AddLog(types.EthTransferLog(sender, recipient, amount)) + } +} diff --git a/core/gaspool.go b/core/gaspool.go index 564f059016..14f5abd93c 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -21,43 +21,87 @@ import ( "math" ) -// GasPool tracks the amount of gas available during execution of the transactions -// in a block. The zero value is a pool with zero gas available. -type GasPool uint64 - -// AddGas makes gas available for execution. -func (gp *GasPool) AddGas(amount uint64) *GasPool { - if uint64(*gp) > math.MaxUint64-amount { - panic("gas pool pushed above uint64") - } - - *(*uint64)(gp) += amount +// GasPool tracks the amount of gas available for transaction execution +// within a block, along with the cumulative gas consumed. +type GasPool struct { + remaining uint64 + initial uint64 + cumulativeUsed uint64 +} - return gp +// NewGasPool initializes the gasPool with the given amount. +func NewGasPool(amount uint64) *GasPool { + return &GasPool{ + remaining: amount, + initial: amount, + } } // SubGas deducts the given amount from the pool if enough gas is // available and returns an error otherwise. func (gp *GasPool) SubGas(amount uint64) error { - if uint64(*gp) < amount { + if gp.remaining < amount { return ErrGasLimitReached } + gp.remaining -= amount + return nil +} - *(*uint64)(gp) -= amount +// ReturnGas adds the refunded gas back to the pool and updates +// the cumulative gas usage accordingly. +func (gp *GasPool) ReturnGas(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) + gp.remaining += returned + // gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost) + // regardless of Amsterdam is activated or not. + gp.cumulativeUsed += gasUsed return nil } // Gas returns the amount of gas remaining in the pool. func (gp *GasPool) Gas() uint64 { - return uint64(*gp) + return gp.remaining +} + +// CumulativeUsed returns the amount of cumulative consumed gas (refunded included). +func (gp *GasPool) CumulativeUsed() uint64 { + return gp.cumulativeUsed +} + +// Used returns the amount of consumed gas. +func (gp *GasPool) Used() uint64 { + if gp.initial < gp.remaining { + panic("gas used underflow") + } + return gp.initial - gp.remaining +} + +// 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, + } } -// SetGas sets the amount of gas with the provided number. -func (gp *GasPool) SetGas(gas uint64) { - *(*uint64)(gp) = gas +// Set sets the content of gasPool with the provided one. +func (gp *GasPool) Set(other *GasPool) { + gp.initial = other.initial + gp.remaining = other.remaining + gp.cumulativeUsed = other.cumulativeUsed } func (gp *GasPool) String() string { - return fmt.Sprintf("%d", *gp) + return fmt.Sprintf("initial: %d, remaining: %d, cumulative used: %d", gp.initial, gp.remaining, gp.cumulativeUsed) } diff --git a/core/genesis_test.go b/core/genesis_test.go index 25064331b3..de40172680 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -331,7 +331,7 @@ func TestVerkleGenesisCommit(t *testing.T) { }, } - expected := common.FromHex("b94812c1674dcf4f2bc98f4503d15f4cc674265135bcf3be6e4417b60881042a") + expected := common.FromHex("1fd154971d9a386c4ec75fe7138c17efb569bfc2962e46e94a376ba997e3fadc") got := genesis.ToBlock().Root().Bytes() if !bytes.Equal(got, expected) { t.Fatalf("invalid genesis state root, expected %x, got %x", expected, got) diff --git a/core/history/historymode.go b/core/history/historymode.go index e735222d37..bdaf07826d 100644 --- a/core/history/historymode.go +++ b/core/history/historymode.go @@ -32,10 +32,13 @@ const ( // KeepPostMerge sets the history pruning point to the merge activation block. KeepPostMerge + + // KeepPostPrague sets the history pruning point to the Prague (Pectra) activation block. + KeepPostPrague ) func (m HistoryMode) IsValid() bool { - return m <= KeepPostMerge + return m <= KeepPostPrague } func (m HistoryMode) String() string { @@ -44,6 +47,8 @@ func (m HistoryMode) String() string { return "all" case KeepPostMerge: return "postmerge" + case KeepPostPrague: + return "postprague" default: return fmt.Sprintf("invalid HistoryMode(%d)", m) } @@ -64,8 +69,10 @@ func (m *HistoryMode) UnmarshalText(text []byte) error { *m = KeepAll case "postmerge": *m = KeepPostMerge + case "postprague": + *m = KeepPostPrague default: - return fmt.Errorf(`unknown sync mode %q, want "all" or "postmerge"`, text) + return fmt.Errorf(`unknown history mode %q, want "all", "postmerge", or "postprague"`, text) } return nil } @@ -75,10 +82,10 @@ type PrunePoint struct { BlockHash common.Hash } -// PrunePoints the pre-defined history pruning cutoff blocks for known networks. +// MergePrunePoints contains the pre-defined history pruning cutoff blocks for known networks. // They point to the first post-merge block. Any pruning should truncate *up to* but excluding -// given block. -var PrunePoints = map[common.Hash]*PrunePoint{ +// the given block. +var MergePrunePoints = map[common.Hash]*PrunePoint{ // mainnet params.MainnetGenesisHash: { BlockNumber: 15537393, @@ -91,6 +98,39 @@ var PrunePoints = map[common.Hash]*PrunePoint{ }, } +// PraguePrunePoints contains the pre-defined history pruning cutoff blocks for the Prague +// (Pectra) upgrade. They point to the first post-Prague block. Any pruning should truncate +// *up to* but excluding the given block. +var PraguePrunePoints = map[common.Hash]*PrunePoint{ + // mainnet - first Prague block (May 7, 2025) + params.MainnetGenesisHash: { + BlockNumber: 22431084, + BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"), + }, + // sepolia - first Prague block (March 5, 2025) + params.SepoliaGenesisHash: { + BlockNumber: 7836331, + BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"), + }, +} + +// PrunePoints is an alias for MergePrunePoints for backward compatibility. +// Deprecated: Use GetPrunePoint or MergePrunePoints directly. +var PrunePoints = MergePrunePoints + +// GetPrunePoint returns the prune point for the given genesis hash and history mode. +// Returns nil if no prune point is defined for the given combination. +func GetPrunePoint(genesisHash common.Hash, mode HistoryMode) *PrunePoint { + switch mode { + case KeepPostMerge: + return MergePrunePoints[genesisHash] + case KeepPostPrague: + return PraguePrunePoints[genesisHash] + default: + return nil + } +} + // PrunedHistoryError is returned by APIs when the requested history is pruned. type PrunedHistoryError struct{} diff --git a/core/mainnet_witness_benchmark_test.go b/core/mainnet_witness_benchmark_test.go index 2292306801..05e31f0c06 100644 --- a/core/mainnet_witness_benchmark_test.go +++ b/core/mainnet_witness_benchmark_test.go @@ -218,7 +218,7 @@ func loadWitnessFromJSON(path string) (*stateless.Witness, error) { contextHeader.Root = common.Hash{} contextHeader.ReceiptHash = common.Hash{} - witness, err := stateless.NewWitness(&contextHeader, nil) + witness, err := stateless.NewWitness(&contextHeader, nil, false) if err != nil { return nil, fmt.Errorf("creating witness: %w", err) } @@ -1285,7 +1285,7 @@ func processV2Parallel(pb *preparedBlock, config *params.ChainConfig, engine con results[taskIdx] = v2TxResult{txIdx: t.idx, pdb: pdb, tx: t.tx, err: fmt.Errorf("panic: %v", r)} } }() - _, err := ApplyMessage(evm, t.msg, new(GasPool).AddGas(pb.block.GasLimit())) + _, err := ApplyMessage(evm, t.msg, NewGasPool(pb.block.GasLimit())) results[taskIdx] = v2TxResult{txIdx: t.idx, pdb: pdb, tx: t.tx, err: err} }() } @@ -1495,7 +1495,7 @@ func TestV2ChainWaitDiagnostic(t *testing.T) { tasks = append(tasks, V2Task{Index: j, Tx: tx, Msg: msg}) } - v2BaseDB.StartPrefetcher("diag", nil, nil) + v2BaseDB.StartPrefetcher("diag", nil) result := ExecuteV2BlockSTM(context.Background(), tasks, readBase, store, bals, blockCtx, bd.block.Hash(), vm.Config{}, config, bd.block.GasLimit(), 8, v2BaseDB, nil) v2BaseDB.StopPrefetcher() @@ -1669,7 +1669,7 @@ func BenchmarkV2AllBlocks(b *testing.B) { // safeBase.CollectCodeWitness). func processV2BlockSTMWithWitness(pb *preparedBlock, config *params.ChainConfig, engine consensus.Engine, numWorkers int) error { db := pb.baseState.Copy() - w, err := stateless.NewWitness(pb.block.Header(), nil) + w, err := stateless.NewWitness(pb.block.Header(), nil, false) if err != nil { return err } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index c00e550064..3210228648 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -148,11 +148,11 @@ func (task *ExecutionTask) setupEVM(mvh *blockstm.MVHashMap, incarnation int) *v func (task *ExecutionTask) runMessage(evm *vm.EVM) error { if !*task.shouldDelayFeeCal { var err error - task.result, err = ApplyMessage(evm, &task.msg, new(GasPool).AddGas(task.gasLimit)) + task.result, err = ApplyMessage(evm, &task.msg, NewGasPool(task.gasLimit)) return err } var err error - task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, NewGasPool(task.gasLimit)) if task.result == nil || err != nil { return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} } @@ -735,7 +735,7 @@ func (e *v2Env) applyMessage(t *v2Task, evm *vm.EVM, pdb *state.ParallelStateDB) pdb.Panicked = true } }() - result, execErr := ApplyMessageNoFeeLog(evm, t.msg, new(GasPool).AddGas(e.gasLimit)) + result, execErr := ApplyMessageNoFeeLog(evm, t.msg, NewGasPool(e.gasLimit)) if result == nil { // Consensus-level error (bad nonce, insufficient upfront gas, intrinsic // gas underflow, blob fork-gating, etc.). Serial returns this as a @@ -1144,7 +1144,7 @@ func (p *V2StateProcessor) Process(block *types.Block, statedb *state.StateDB, c // execution — the produced witness would land empty. prevWitness := finalDB.Witness() finalDB.StopPrefetcher() - finalDB.StartPrefetcher("v2_settle", prevWitness, nil) + finalDB.StartPrefetcher("v2_settle", prevWitness) finalDB.SkipTimers() // Copy() deep-copies the witness; re-share so BLOCKHASH writes reach finalDB. readBase := statedb.Copy() diff --git a/core/parallel_state_processor_review_test.go b/core/parallel_state_processor_review_test.go index ec068eefa6..636a5e94ff 100644 --- a/core/parallel_state_processor_review_test.go +++ b/core/parallel_state_processor_review_test.go @@ -326,7 +326,7 @@ func TestV2StateProcessor_PanickedTxFailsBlock(t *testing.T) { }} finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() result := ExecuteV2BlockSTM(context.Background(), tasks, base, store, bals, blockCtx, common.Hash{}, cfg, chainConfig, @@ -404,7 +404,7 @@ func TestV2ApplyMessage_FirstIncarnationPanicLogsDebug(t *testing.T) { }} finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() _ = ExecuteV2BlockSTM(context.Background(), tasks, base, @@ -491,7 +491,7 @@ func TestV2StateProcessor_ProducesWitness(t *testing.T) { bals := blockstm.NewMVBalanceStore() finalDB := base - finalDB.StartPrefetcher("test", w, nil) + finalDB.StartPrefetcher("test", w) defer finalDB.StopPrefetcher() result := ExecuteV2BlockSTM(context.Background(), tasks, base, store, bals, blockCtx, common.Hash{}, vm.Config{}, chainConfig, @@ -564,7 +564,7 @@ func TestExecuteV2BlockSTM_MidFlightCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() store := blockstm.NewMVStore() bals := blockstm.NewMVBalanceStore() @@ -645,7 +645,7 @@ func TestExecuteV2BlockSTM_HonoursCancellation(t *testing.T) { cancel() finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() store := blockstm.NewMVStore() @@ -744,7 +744,7 @@ func TestV2StateProcessor_ReceiptHasBlockHash(t *testing.T) { bals := blockstm.NewMVBalanceStore() finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() result := ExecuteV2BlockSTM(context.Background(), tasks, base, store, bals, blockCtx, blockHash, vm.Config{}, chainConfig, @@ -813,7 +813,7 @@ func TestV2StateProcessor_ApplyMessageErrorFailsBlock(t *testing.T) { bals := blockstm.NewMVBalanceStore() finalDB := base.Copy() - finalDB.StartPrefetcher("test", nil, nil) + finalDB.StartPrefetcher("test", nil) defer finalDB.StopPrefetcher() result := ExecuteV2BlockSTM(context.Background(), tasks, base, store, bals, blockCtx, common.Hash{}, vm.Config{}, chainConfig, diff --git a/core/rawdb/ancienttest/testsuite.go b/core/rawdb/ancienttest/testsuite.go index 7512c1f44b..eb66645a3a 100644 --- a/core/rawdb/ancienttest/testsuite.go +++ b/core/rawdb/ancienttest/testsuite.go @@ -260,6 +260,46 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { if err != nil { t.Fatalf("Failed to write ancient data %v", err) } + + // Write should work after truncating from tail but over the head + db.TruncateTail(200) + head, err := db.Ancients() + if err != nil { + t.Fatalf("Failed to retrieve head ancients %v", err) + } + tail, err := db.Tail() + if err != nil { + t.Fatalf("Failed to retrieve tail ancients %v", err) + } + if head != 200 || tail != 200 { + t.Fatalf("Ancient head and tail are not expected") + } + _, err = db.ModifyAncients(func(op ethdb.AncientWriteOp) error { + offset := uint64(200) + for i := 0; i < 100; i++ { + if err := op.AppendRaw("a", offset+uint64(i), dataA[i]); err != nil { + return err + } + if err := op.AppendRaw("b", offset+uint64(i), dataB[i]); err != nil { + return err + } + } + return nil + }) + if err != nil { + t.Fatalf("Failed to write ancient data %v", err) + } + head, err = db.Ancients() + if err != nil { + t.Fatalf("Failed to retrieve head ancients %v", err) + } + tail, err = db.Tail() + if err != nil { + t.Fatalf("Failed to retrieve tail ancients %v", err) + } + if head != 300 || tail != 200 { + t.Fatalf("Ancient head and tail are not expected") + } } func nonMutable(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index adbbf0238b..8751487d83 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -715,9 +715,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { bodies.add(size) case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength): receipts.add(size) - case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix): + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix) && len(key) == (len(headerPrefix)+8+common.HashLength+len(headerTDSuffix)): tds.add(size) - case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix): + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix) && len(key) == (len(headerPrefix)+8+len(headerHashSuffix)): numHashPairings.add(size) case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): hashNumPairings.add(size) diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 0624139344..d33fd8ec19 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -62,7 +62,7 @@ const freezerTableSize = 2 * 1000 * 1000 * 1000 // - The in-order data ensures that disk reads are always optimized. type Freezer struct { datadir string - frozen atomic.Uint64 // Number of items already frozen + head atomic.Uint64 // Number of items stored (including items removed from tail) tail atomic.Uint64 // Number of the first stored item in the freezer // This lock synchronizes writers and the truncate operation, as well as @@ -102,12 +102,12 @@ func NewFreezer(datadir string, namespace string, readonly bool, offset uint64, return nil, errSymlinkDatadir } } + // Leveldb/Pebble uses LOCK as the filelock filename. To prevent the + // name collision, we use FLOCK as the lock name. flockFile := filepath.Join(datadir, "FLOCK") if err := os.MkdirAll(filepath.Dir(flockFile), 0755); err != nil { return nil, err } - // Leveldb uses LOCK as the filelock filename. To prevent the - // name collision, we use FLOCK as the lock name. lock := flock.New(flockFile) tryLock := lock.TryLock if readonly { @@ -159,12 +159,12 @@ func NewFreezer(datadir string, namespace string, readonly bool, offset uint64, // Some blocks in ancientDB may have already been frozen and been pruned, so adding the offset to // represent the absolute number of blocks already frozen. - freezer.frozen.Add(offset) + freezer.head.Add(offset) // Create the write batch. freezer.writeBatch = newFreezerBatch(freezer) - log.Info("Opened ancient database", "database", datadir, "readonly", readonly, "frozen", freezer.frozen.Load(), "offset", freezer.offset.Load()) + log.Info("Opened ancient database", "database", datadir, "readonly", readonly, "frozen", freezer.head.Load(), "offset", freezer.offset.Load()) return freezer, nil } @@ -224,12 +224,12 @@ func (f *Freezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, // Ancients returns the length of the frozen items. func (f *Freezer) Ancients() (uint64, error) { - return f.frozen.Load(), nil + return f.head.Load(), nil } // ItemAmountInAncient returns the actual length of current ancientDB. func (f *Freezer) ItemAmountInAncient() (uint64, error) { - return f.frozen.Load() - f.offset.Load(), nil + return f.head.Load() - f.offset.Load(), nil } // AncientOffSet returns the offset of current ancientDB. @@ -273,7 +273,7 @@ func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize defer f.writeLock.Unlock() // Roll back all tables to the starting position in case of error. - prevItem := f.frozen.Load() + prevItem := f.head.Load() defer func() { if err != nil { // The write operation has failed. Go back to the previous item position. @@ -294,7 +294,7 @@ func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize if err != nil { return 0, err } - f.frozen.Store(item) + f.head.Store(item) return writeSize, nil } @@ -307,7 +307,7 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { f.writeLock.Lock() defer f.writeLock.Unlock() - oitems := f.frozen.Load() + oitems := f.head.Load() if oitems <= items { return oitems, nil } @@ -316,7 +316,7 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { return 0, err } } - f.frozen.Store(items) + f.head.Store(items) return oitems, nil } @@ -341,6 +341,11 @@ func (f *Freezer) TruncateTail(tail uint64) (uint64, error) { } } f.tail.Store(tail) + + // Update the head if the requested tail exceeds the current head + if f.head.Load() < tail { + f.head.Store(tail) + } return old, nil } @@ -400,7 +405,7 @@ func (f *Freezer) validate() error { prunedTail = &tmp } - f.frozen.Store(head) + f.head.Store(head) f.tail.Store(*prunedTail) return nil } @@ -435,7 +440,7 @@ func (f *Freezer) repair() error { } } - f.frozen.Store(head) + f.head.Store(head) f.tail.Store(prunedTail) return nil } diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index 962340cbaf..0578288b2f 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -114,7 +114,7 @@ func (t *memoryTable) truncateTail(items uint64) error { return nil } if t.items < items { - return errors.New("truncation above head") + return t.reset(items) } for i := uint64(0); i < items-t.offset; i++ { if t.size > uint64(len(t.data[i])) { @@ -128,6 +128,16 @@ func (t *memoryTable) truncateTail(items uint64) error { return nil } +// reset clears the entire table and sets both the head and tail to the given +// value. It assumes the caller holds the lock and that tail > t.items. +func (t *memoryTable) reset(offset uint64) error { + t.size = 0 + t.data = nil + t.items = offset + t.offset = offset + return nil +} + // commit merges the given item batch into table. It's presumed that the // batch is ordered and continuous with table. func (t *memoryTable) commit(batch [][]byte) error { @@ -406,6 +416,9 @@ func (f *MemoryFreezer) TruncateTail(tail uint64) (uint64, error) { } } f.tail = tail + if f.items < tail { + f.items = tail + } return old, nil } diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 38bc3cc390..7efd7b3fe1 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -157,7 +157,7 @@ func (f *resettableFreezer) AncientOffSet() uint64 { // ItemAmountInAncient returns the actual length of current ancientDB. func (f *resettableFreezer) ItemAmountInAncient() (uint64, error) { - return f.freezer.frozen.Load() - f.freezer.offset.Load(), nil + return f.freezer.head.Load() - f.freezer.offset.Load(), nil } // Tail returns the number of first stored item in the freezer. diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index b53d8daa17..e1e5bedb7e 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -740,13 +740,14 @@ func (t *freezerTable) truncateTail(items uint64) error { t.lock.Lock() defer t.lock.Unlock() - // Ensure the given truncate target falls in the correct range + // Short-circuit if the requested tail deletion points to a stale position if t.itemHidden.Load() >= items { return nil } + // If the requested tail exceeds the current head, reset the entire table if t.items.Load() < items { - return errors.New("truncation above head") + return t.resetTo(items) } // Load the new tail index by the given new tail position var ( @@ -864,10 +865,9 @@ func (t *freezerTable) truncateTail(items uint64) error { shorten := indexEntrySize * int64(newDeleted-deleted) if t.metadata.flushOffset <= shorten { return fmt.Errorf("invalid index flush offset: %d, shorten: %d", t.metadata.flushOffset, shorten) - } else { - if err := t.metadata.setFlushOffset(t.metadata.flushOffset-shorten, true); err != nil { - return err - } + } + if err := t.metadata.setFlushOffset(t.metadata.flushOffset-shorten, true); err != nil { + return err } // Retrieve the new size and update the total size counter newSize, err := t.sizeNolock() @@ -880,6 +880,59 @@ func (t *freezerTable) truncateTail(items uint64) error { return nil } +// resetTo clears the entire table and sets both the head and tail to the given +// value. It assumes the caller holds the lock and that tail > t.items. +func (t *freezerTable) resetTo(tail uint64) error { + // Sync the entire table before resetting, eliminating the potential + // data corruption. + err := t.doSync() + if err != nil { + return err + } + // Update the index file to reflect the new offset + if err := t.index.Close(); err != nil { + return err + } + entry := &indexEntry{ + filenum: t.headId + 1, + offset: uint32(tail), + } + if err := reset(t.index.Name(), entry.append(nil)); err != nil { + return err + } + if err := t.metadata.setVirtualTail(tail, true); err != nil { + return err + } + if err := t.metadata.setFlushOffset(indexEntrySize, true); err != nil { + return err + } + t.index, err = openFreezerFileForAppend(t.index.Name()) + if err != nil { + return err + } + + // Purge all the existing data file + if err := t.head.Close(); err != nil { + return err + } + t.headId = t.headId + 1 + t.tailId = t.headId + t.headBytes = 0 + + t.head, err = t.openFile(t.headId, openFreezerFileTruncated) + if err != nil { + return err + } + t.releaseFilesBefore(t.headId, true) + + t.items.Store(tail) + t.itemOffset.Store(tail) + t.itemHidden.Store(tail) + t.sizeGauge.Update(0) + + return nil +} + // Close closes all opened files and finalizes the freezer table for use. // This operation must be completed before shutdown to prevent the loss of // recent writes. @@ -1322,26 +1375,21 @@ func (t *freezerTable) doSync() error { return errClosed } - var err error - - trackError := func(e error) { - if e != nil && err == nil { - err = e - } + if err := t.index.Sync(); err != nil { + return err } - trackError(t.index.Sync()) - trackError(t.head.Sync()) + if err := t.head.Sync(); err != nil { + return err + } // A crash may occur before the offset is updated, leaving the offset - // points to a old position. If so, the extra items above the offset + // points to an old position. If so, the extra items above the offset // will be truncated during the next run. stat, err := t.index.Stat() if err != nil { return err } - offset := stat.Size() - trackError(t.metadata.setFlushOffset(offset, true)) - return err + return t.metadata.setFlushOffset(stat.Size(), true) } func (t *freezerTable) dumpIndexStdout(start, stop int64) { diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go index aeb9c59b26..9b55d9c510 100644 --- a/core/rawdb/freezer_table_test.go +++ b/core/rawdb/freezer_table_test.go @@ -1215,6 +1215,7 @@ const ( opTruncateHeadAll opTruncateTail opTruncateTailAll + opTruncateTailOverHead opCheckAll opMax // boundary value, not an actual op ) @@ -1306,6 +1307,11 @@ func (randTest) Generate(r *rand.Rand, size int) reflect.Value { step.target = deleted + uint64(len(items)) items = items[:0] deleted = step.target + case opTruncateTailOverHead: + newDeleted := deleted + uint64(len(items)) + 10 + step.target = newDeleted + deleted = newDeleted + items = items[:0] } steps = append(steps, step) @@ -1353,7 +1359,7 @@ func runRandTest(rt randTest) bool { for i := 0; i < len(step.items); i++ { batch.AppendRaw(step.items[i], step.blobs[i]) } - batch.commit() + rt[i].err = batch.commit() values = append(values, step.blobs...) @@ -1379,25 +1385,29 @@ func runRandTest(rt randTest) bool { } case opTruncateHead: - f.truncateHead(step.target) + rt[i].err = f.truncateHead(step.target) length := f.items.Load() - f.itemHidden.Load() values = values[:length] case opTruncateHeadAll: - f.truncateHead(step.target) + rt[i].err = f.truncateHead(step.target) values = nil case opTruncateTail: prev := f.itemHidden.Load() - f.truncateTail(step.target) + rt[i].err = f.truncateTail(step.target) truncated := f.itemHidden.Load() - prev values = values[truncated:] case opTruncateTailAll: - f.truncateTail(step.target) + rt[i].err = f.truncateTail(step.target) + values = nil + + case opTruncateTailOverHead: + rt[i].err = f.truncateTail(step.target) values = nil } @@ -1727,3 +1737,43 @@ func TestFreezerAncientBytes(t *testing.T) { }) } } + +func TestTruncateOverHead(t *testing.T) { + t.Parallel() + + fn := fmt.Sprintf("t-%d", rand.Uint64()) + f, err := newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + + // Tail truncation on an empty table + if err := f.truncateTail(10); err != nil { + t.Fatal(err) + } + batch := f.newBatch(0) + data := getChunk(10, 1) + require.NoError(t, batch.AppendRaw(uint64(10), data)) + require.NoError(t, batch.commit()) + + got, err := f.RetrieveItems(uint64(10), 1, 0) + require.NoError(t, err) + if !bytes.Equal(got[0], data) { + t.Fatalf("Unexpected bytes, want: %v, got: %v", data, got[0]) + } + + // Tail truncation on the non-empty table + if err := f.truncateTail(20); err != nil { + t.Fatal(err) + } + batch = f.newBatch(0) + data = getChunk(10, 1) + require.NoError(t, batch.AppendRaw(uint64(20), data)) + require.NoError(t, batch.commit()) + + got, err = f.RetrieveItems(uint64(20), 1, 0) + require.NoError(t, err) + if !bytes.Equal(got[0], data) { + t.Fatalf("Unexpected bytes, want: %v, got: %v", data, got[0]) + } +} diff --git a/core/rawdb/freezer_utils.go b/core/rawdb/freezer_utils.go index 13012fcef4..6e6792a2d5 100644 --- a/core/rawdb/freezer_utils.go +++ b/core/rawdb/freezer_utils.go @@ -22,6 +22,13 @@ import ( "path/filepath" ) +func atomicRename(src, dest string) error { + if err := os.Rename(src, dest); err != nil { + return err + } + return syncDir(filepath.Dir(src)) +} + // copyFrom copies data from 'srcPath' at offset 'offset' into 'destPath'. // The 'destPath' is created if it doesn't exist, otherwise it is overwritten. // Before the copy is executed, there is a callback can be registered to @@ -77,13 +84,48 @@ func copyFrom(srcPath, destPath string, offset uint64, before func(f *os.File) e } f = nil - return os.Rename(fname, destPath) + + return atomicRename(fname, destPath) +} + +// reset atomically replaces the file at the given path with the provided content. +func reset(path string, content []byte) error { + // Create a temp file in the same dir where we want it to wind up + f, err := os.CreateTemp(filepath.Dir(path), "*") + if err != nil { + return err + } + fname := f.Name() + + // Clean up the leftover file + defer func() { + if f != nil { + f.Close() + } + os.Remove(fname) + }() + + // Write the content into the temp file + _, err = f.Write(content) + if err != nil { + return err + } + // Permanently persist the content into disk + if err := f.Sync(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + f = nil + + return atomicRename(fname, path) } // openFreezerFileForAppend opens a freezer table file and seeks to the end func openFreezerFileForAppend(filename string) (*os.File, error) { // Open the file without the O_APPEND flag - // because it has differing behaviour during Truncate operations + // because it has differing behavior during Truncate operations // on different OS's file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) if err != nil { diff --git a/core/rawdb/freezer_utils_unix.go b/core/rawdb/freezer_utils_unix.go new file mode 100644 index 0000000000..1d26490cab --- /dev/null +++ b/core/rawdb/freezer_utils_unix.go @@ -0,0 +1,49 @@ +// Copyright 2022 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 !windows +// +build !windows + +package rawdb + +import ( + "errors" + "os" + "syscall" +) + +// syncDir ensures that the directory metadata (e.g. newly renamed files) +// is flushed to durable storage. +func syncDir(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + + // Some file systems do not support fsyncing directories (e.g. some FUSE + // mounts). Ignore EINVAL in those cases. + if err := f.Sync(); err != nil { + if errors.Is(err, os.ErrInvalid) { + return nil + } + if patherr, ok := err.(*os.PathError); ok && patherr.Err == syscall.EINVAL { + return nil + } + return err + } + return nil +} diff --git a/core/rawdb/freezer_utils_windows.go b/core/rawdb/freezer_utils_windows.go new file mode 100644 index 0000000000..7b652f7ab5 --- /dev/null +++ b/core/rawdb/freezer_utils_windows.go @@ -0,0 +1,26 @@ +// Copyright 2022 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 windows +// +build windows + +package rawdb + +// syncDir is a no-op on Windows. Fsyncing a directory handle is not +// supported and returns "Access is denied". +func syncDir(name string) error { + return nil +} diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 32a1b1fa1a..6ebe8dd662 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -264,6 +264,11 @@ func (b *tableBatch) Reset() { b.batch.Reset() } +// Close closes the batch and releases all associated resources. +func (b *tableBatch) Close() { + b.batch.Close() +} + // tableReplayer is a wrapper around a batch replayer which truncates // the added prefix. type tableReplayer struct { diff --git a/core/state/parallel_statedb.go b/core/state/parallel_statedb.go index 5d7579f862..1c17803027 100644 --- a/core/state/parallel_statedb.go +++ b/core/state/parallel_statedb.go @@ -1194,6 +1194,23 @@ func (s *ParallelStateDB) AddPreimage(hash common.Hash, preimage []byte) { func (s *ParallelStateDB) Logs() []*types.Log { return s.logs } +// EmitLogsForBurnAccounts mirrors StateDB.EmitLogsForBurnAccounts: an account +// destructed by this tx can still receive funds afterwards, and that residual +// balance is burned at removal, so EIP-7708 wants a burn log for it. The +// address sort keeps the log order identical to the serial executor's. +func (s *ParallelStateDB) EmitLogsForBurnAccounts() { + var list []common.Address + for addr, destructed := range s.destructed { + if destructed && !s.GetBalance(addr).IsZero() { + list = append(list, addr) + } + } + slices.SortFunc(list, func(a, b common.Address) int { return a.Cmp(b) }) + for _, addr := range list { + s.AddLog(types.EthBurnLog(addr, s.GetBalance(addr))) + } +} + // ---------- Prepare ---------- func (s *ParallelStateDB) Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) { diff --git a/core/state/reader.go b/core/state/reader.go index 59c1724e28..987641d23b 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -184,7 +184,7 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { if v, ok := r.addrCache.Load(addr); ok { addrHash = v.(common.Hash) } else { - addrHash = crypto.Keccak256Hash(addr.Bytes()) + addrHash = crypto.Keccak256Hash(addr[:]) r.addrCache.Store(addr, addrHash) } account, err := r.reader.Account(addrHash) @@ -221,10 +221,10 @@ func (r *flatReader) Storage(addr common.Address, key common.Hash) (common.Hash, if v, ok := r.addrCache.Load(addr); ok { addrHash = v.(common.Hash) } else { - addrHash = crypto.Keccak256Hash(addr.Bytes()) + addrHash = crypto.Keccak256Hash(addr[:]) r.addrCache.Store(addr, addrHash) } - slotHash := crypto.Keccak256Hash(key.Bytes()) + slotHash := crypto.Keccak256Hash(key[:]) ret, err := r.reader.Storage(addrHash, slotHash) if err != nil { return common.Hash{}, err @@ -381,13 +381,14 @@ func (r *trieReader) EnableConcurrentReads() { // every worker read accumulates in the same set of trie tracers. Walking // reader.subTries here picks up exactly the worker-only-read tries that // finalDB doesn't know about. -func (r *trieReader) CollectStateWitness(addState func(map[string][]byte)) { +func (r *trieReader) CollectStateWitness(addState func(map[string][]byte, common.Hash)) { if r.mainTrie != nil { - addState(r.mainTrie.Witness()) + addState(r.mainTrie.Witness(), common.Hash{}) } - r.subTries.Range(func(_, v any) bool { + r.subTries.Range(func(k, v any) bool { if t, ok := v.(interface{ Witness() map[string][]byte }); ok { - addState(t.Witness()) + addr, _ := k.(common.Address) + addState(t.Witness(), crypto.Keccak256Hash(addr[:])) } return true }) diff --git a/core/state/state_object.go b/core/state/state_object.go index 8d0a68e976..d72f27135d 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -479,6 +479,14 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { s.origin = s.data.Copy() return op, nil, nil } + // In Verkle/binary trie mode, all state objects share one unified trie. + // The main account trie commit in stateDB.commit() already calls + // CollectNodes on this trie, so calling Commit here again would + // redundantly traverse and serialize the entire tree per dirty account. + if s.db.GetTrie().IsVerkle() { + s.origin = s.data.Copy() + return op, nil, nil + } root, nodes := s.trie.Commit(false) s.data.Root = root s.origin = s.data.Copy() diff --git a/core/state/statedb.go b/core/state/statedb.go index 274d972149..659740e123 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -22,6 +22,7 @@ import ( "fmt" "maps" "slices" + "sort" "sync" "sync/atomic" "time" @@ -148,8 +149,7 @@ type StateDB struct { journal *journal // State witness if cross validation is needed - witness *stateless.Witness - witnessStats *stateless.WitnessStats + witness *stateless.Witness // witnessPrewalkStop stops the read-set prewalker started by // StartWitnessReadSetPrewalk; CollectStateWitness invokes it before // collecting. Idempotent. Deliberately not carried across Copy. @@ -396,7 +396,7 @@ func (s *StateDB) StartWitnessReadSetPrewalk() (stop func()) { return stop } -func collectStateWitnessFromReader(r any, addState func(map[string][]byte)) { +func collectStateWitnessFromReader(r any, addState func(map[string][]byte, common.Hash)) { switch v := r.(type) { case *reader: collectStateWitnessFromReader(v.StateReader, addState) @@ -731,13 +731,12 @@ func (s *StateDB) SetWitness(witness *stateless.Witness) { // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. -func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness, witnessStats *stateless.WitnessStats) { +func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) { // Terminate any previously running prefetcher s.StopPrefetcher() // Enable witness collection if requested s.witness = witness - s.witnessStats = witnessStats // With the switch to the Proof-of-Stake consensus algorithm, block production // rewards are now handled at the consensus layer. Consequently, a block may @@ -1471,6 +1470,41 @@ func (s *StateDB) GetRefund() uint64 { return s.refund } +type removedAccountWithBalance struct { + address common.Address + balance *uint256.Int +} + +// EmitLogsForBurnAccounts emits the eth burn logs for accounts scheduled for +// removal which still have positive balance. The purpose of this function is +// to handle a corner case of EIP-7708 where a self-destructed account might +// still receive funds between sending/burning its previous balance and actual +// removal. In this case the burning of these remaining balances still need to +// be logged. +// Specification EIP-7708: https://eips.ethereum.org/EIPS/eip-7708 +// +// This function should only be invoked at the transaction boundary, specifically +// before the Finalise. +func (s *StateDB) EmitLogsForBurnAccounts() { + var list []removedAccountWithBalance + for addr := range s.journal.dirties { + if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() { + list = append(list, removedAccountWithBalance{ + address: obj.address, + balance: obj.Balance(), + }) + } + } + if list != nil { + sort.Slice(list, func(i, j int) bool { + return list[i].address.Cmp(list[j].address) < 0 + }) + } + for _, acct := range list { + s.AddLog(types.EthBurnLog(acct.address, acct.balance)) + } +} + // Finalise finalises the state by removing the destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. @@ -1559,32 +1593,65 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { start = time.Now() } if s.db.TrieDB().IsVerkle() { - // Whilst MPT storage tries are independent, Verkle has one single trie - // for all the accounts and all the storage slots merged together. The - // former can thus be simply parallelized, but updating the latter will - // need concurrency support within the trie itself. That's a TODO for a - // later time. - workers.SetLimit(1) - } - for addr, op := range s.mutations { - if op.applied || op.isDelete() { - continue + // Bypass per-account updateTrie() for binary trie. In binary trie mode + // there is only one unified trie (OpenStorageTrie returns self), so the + // per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie, + // prefetcher.used) is redundant overhead. Apply all storage updates + // directly in a single pass. + for addr, op := range s.mutations { + if op.applied || op.isDelete() { + continue + } + obj := s.stateObjects[addr] + if len(obj.uncommittedStorage) == 0 { + continue + } + for key, origin := range obj.uncommittedStorage { + value, exist := obj.pendingStorage[key] + if value == origin || !exist { + continue + } + if (value != common.Hash{}) { + if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil { + s.setError(err) + } + } else { + if err := s.trie.DeleteStorage(addr, key[:]); err != nil { + s.setError(err) + } + } + } } - obj := s.stateObjects[addr] // closure for the task runner below - workers.Go(func() error { - if s.db.TrieDB().IsVerkle() { - obj.updateTrie() - } else { + // Clear uncommittedStorage and assign trie on each touched object. + // obj.trie must be set because this path bypasses updateTrie(), which + // is where obj.trie normally gets lazily loaded via getTrie(). + for addr, op := range s.mutations { + if op.applied || op.isDelete() { + continue + } + obj := s.stateObjects[addr] + if len(obj.uncommittedStorage) > 0 { + obj.uncommittedStorage = make(Storage) + } + obj.trie = s.trie + } + } else { + for addr, op := range s.mutations { + if op.applied || op.isDelete() { + continue + } + obj := s.stateObjects[addr] // closure for the task runner below + workers.Go(func() error { obj.updateRoot() // If witness building is enabled and the state object has a trie, // gather the witnesses for its specific storage trie if s.witness != nil && obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.addrHash()) } - } - return nil - }) + return nil + }) + } } // If witness building is enabled, gather all the read-only accesses. // Skip witness collection in Verkle mode, they will be gathered @@ -1598,17 +1665,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - witness := trie.Witness() - s.witness.AddState(witness) - if s.witnessStats != nil { - s.witnessStats.Add(witness, obj.addrHash()) - } + s.witness.AddState(trie.Witness(), obj.addrHash()) } else if obj.trie != nil { - witness := obj.trie.Witness() - s.witness.AddState(witness) - if s.witnessStats != nil { - s.witnessStats.Add(witness, obj.addrHash()) - } + s.witness.AddState(obj.trie.Witness(), obj.addrHash()) } } // Pull in only-read and non-destructed trie witnesses @@ -1622,17 +1681,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - witness := trie.Witness() - s.witness.AddState(witness) - if s.witnessStats != nil { - s.witnessStats.Add(witness, obj.addrHash()) - } + s.witness.AddState(trie.Witness(), obj.addrHash()) } else if obj.trie != nil { - witness := obj.trie.Witness() - s.witness.AddState(witness) - if s.witnessStats != nil { - s.witnessStats.Add(witness, obj.addrHash()) - } + s.witness.AddState(obj.trie.Witness(), obj.addrHash()) } } s.WitnessCollection += time.Since(witStart) @@ -1652,7 +1703,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { if !s.skipTimers { start = time.Now() } - if s.prefetcher != nil { + if s.prefetcher != nil && !s.db.TrieDB().IsVerkle() { if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil { log.Error("Failed to retrieve account pre-fetcher trie") } else { @@ -1708,11 +1759,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather the account trie witness if s.witness != nil { witStart := time.Now() - witness := s.trie.Witness() - s.witness.AddState(witness) - if s.witnessStats != nil { - s.witnessStats.Add(witness, common.Hash{}) - } + s.witness.AddState(s.trie.Witness(), common.Hash{}) s.WitnessCollection += time.Since(witStart) } return hash @@ -2075,6 +2122,7 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag if err := batch.Write(); err != nil { return nil, err } + batch.Close() } if !ret.empty() { // If snapshotting is enabled, update the snapshot tree with this new version diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 5e3f301e0e..0c16ebe08f 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -284,6 +284,10 @@ func (s *hookedStateDB) AddLog(log *types.Log) { } } +func (s *hookedStateDB) EmitLogsForBurnAccounts() { + s.inner.EmitLogsForBurnAccounts() +} + func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { defer s.inner.Finalise(deleteEmptyObjects) if s.hooks.OnBalanceChange == nil { diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index d73df475af..e39abd4aa7 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -249,7 +249,7 @@ func (p *StatePrefetcher) prefetchOneTx( evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg) evm.SetInterrupt(interrupt) - result, err := ApplyMessage(evm, msg, new(GasPool).AddGas(header.GasLimit)) + result, err := ApplyMessage(evm, msg, NewGasPool(header.GasLimit)) if err != nil { fails.Add(1) return 0, false diff --git a/core/state_prefetcher_intermediate_root_test.go b/core/state_prefetcher_intermediate_root_test.go index 32f18e2480..f7b7fd5928 100644 --- a/core/state_prefetcher_intermediate_root_test.go +++ b/core/state_prefetcher_intermediate_root_test.go @@ -208,8 +208,7 @@ func runIRTrial(t testing.TB, chain *BlockChain, txs []*types.Transaction, flag // --- Process phase: real ApplyTransactionWithEVM on the main statedb. --- evmCtx := NewEVMBlockContext(header, chain, nil) evm := vm.NewEVM(evmCtx, statedb, chain.Config(), cfg) - gp := new(GasPool).AddGas(header.GasLimit * uint64(len(txs))) - var usedGas uint64 + gp := NewGasPool(header.GasLimit * uint64(len(txs))) procFails := 0 processStart := time.Now() @@ -221,7 +220,7 @@ func runIRTrial(t testing.TB, chain *BlockChain, txs []*types.Transaction, flag continue } if _, err := ApplyTransactionWithEVM( - msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, &usedGas, evm, + msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm, ); err != nil { procFails++ } @@ -560,8 +559,7 @@ func runIRPebbleTrial( // --- Process phase --- evmCtx := NewEVMBlockContext(header, chain, nil) evm := vm.NewEVM(evmCtx, statedb, chain.Config(), cfg) - gp := new(GasPool).AddGas(header.GasLimit * uint64(len(txs))) - var usedGas uint64 + gp := NewGasPool(header.GasLimit * uint64(len(txs))) processStart := time.Now() for i, tx := range txs { @@ -571,7 +569,7 @@ func runIRPebbleTrial( continue } _, _ = ApplyTransactionWithEVM( - msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, &usedGas, evm, + msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm, ) } processDur := time.Since(processStart) diff --git a/core/state_processor.go b/core/state_processor.go index 184a546950..1d49d8d153 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -66,12 +66,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg var ( config = p.chainConfig() receipts types.Receipts - usedGas = new(uint64) header = block.Header() blockHash = block.Hash() blockNumber = block.Number() allLogs []*types.Log - gp = new(GasPool).AddGas(block.GasLimit()) + gp = NewGasPool(block.GasLimit()) err error ) @@ -126,7 +125,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg statedb.SetTxContext(tx.Hash(), i) - receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, usedGas, evm) + receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -200,14 +199,14 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg Receipts: receipts, Requests: requests, Logs: allLogs, - GasUsed: *usedGas, + GasUsed: gp.Used(), }, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (receipt *types.Receipt, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -285,22 +284,22 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } - - *usedGas += result.UsedGas - // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. if statedb.Database().TrieDB().IsVerkle() { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, *usedGas, root), nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil } // MakeReceipt generates the receipt object for a transaction given its execution result. -func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, usedGas uint64, root []byte) *types.Receipt { - // Create a new receipt for the transaction, storing the intermediate root and gas used - // by the tx. - receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: usedGas} +func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, cumulativeGas uint64, root []byte) *types.Receipt { + // Create a new receipt for the transaction, storing the intermediate root + // and gas used by the tx. + // + // The cumulative gas used equals the sum of gasUsed across all preceding + // txs with refunded gas deducted. + receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: cumulativeGas} if result.Failed() { receipt.Status = types.ReceiptStatusFailed } else { @@ -308,6 +307,9 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b } receipt.TxHash = tx.Hash() + + // GasUsed = max(tx_gas_used - gas_refund, calldata_floor_gas_cost), unchanged + // in the Amsterdam fork. receipt.GasUsed = result.UsedGas if tx.Type() == types.BlobTxType { @@ -331,15 +333,15 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // ApplyTransaction attempts to apply a transaction to the given state database // and uses the input parameters for its environment. It returns the receipt -// for the transaction, gas used and an error if the transaction failed, +// for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64) (*types.Receipt, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { return nil, err } // Create a new context to be used in the EVM environment - return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, usedGas, evm) + return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) } // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root diff --git a/core/state_transition.go b/core/state_transition.go index 9af42dc466..0aa50b2516 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -36,7 +36,7 @@ import ( // ExecutionResult includes all output after executing given evm // message no matter the execution itself is successful or not. type ExecutionResult struct { - UsedGas uint64 // Total used gas but include the refunded gas + UsedGas uint64 // Total used gas, refunded gas is deducted MaxUsedGas uint64 // Maximum gas consumed during execution, excluding gas refunds. Err error // Any error encountered during the execution(listed in core/vm/errors.go) ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) @@ -227,6 +227,11 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In // indicates a core error meaning that the message would always fail for that particular // state and would never be accepted within a block. func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { + // Do not panic if the gas pool is nil. This is allowed when executing + // a single message via RPC invocation. + if gp == nil { + gp = NewGasPool(msg.GasLimit) + } evm.SetTxContext(NewEVMTxContext(msg)) return newStateTransition(evm, msg, gp).execute() } @@ -341,8 +346,8 @@ func (st *stateTransition) buyGas() error { st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) } st.gasRemaining = st.msg.GasLimit - st.initialGas = st.msg.GasLimit + mgvalU256, _ := uint256.FromBig(mgval) st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) return nil @@ -535,8 +540,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Check whether the init code size has been exceeded. - if rules.IsShanghai && contractCreation && len(msg.Data) > params.MaxInitCodeSize { - return nil, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(msg.Data), params.MaxInitCodeSize) + 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: @@ -595,8 +602,20 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { 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.initialGas-peakGasUsed, st.gasUsed()) + } else { + // Refund is included for returning + err = st.gp.ReturnGas(st.gasRemaining, st.gasUsed()) + } + if err != nil { + return nil, err + } effectiveTip := msg.GasPrice if rules.IsLondon { @@ -665,7 +684,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ) } } - + if rules.IsAmsterdam { + st.evm.StateDB.EmitLogsForBurnAccounts() + } return &ExecutionResult{ UsedGas: st.gasUsed(), MaxUsedGas: peakGasUsed, @@ -765,10 +786,6 @@ func (st *stateTransition) returnGas() { if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxLeftOverReturned) } - - // Also return remaining gas to the block gas counter so it is - // available for the next transaction. - st.gp.AddGas(st.gasRemaining) } // gasUsed returns the amount of gas used up by the state transition. diff --git a/core/stateless/database_test.go b/core/stateless/database_test.go index 20c570ac41..7c7f828e34 100644 --- a/core/stateless/database_test.go +++ b/core/stateless/database_test.go @@ -226,7 +226,7 @@ func TestMakeHashDBWithDiskDB(t *testing.T) { Root: common.HexToHash("0x5678"), } - witness, err := NewWitness(header, nil) + witness, err := NewWitness(header, nil, false) if err != nil { t.Fatalf("Failed to create witness: %v", err) } @@ -301,7 +301,7 @@ func TestCodePersistenceAcrossWitnesses(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() // First witness import - add some codes - witness1, _ := NewWitness(&types.Header{Number: big.NewInt(100)}, nil) + witness1, _ := NewWitness(&types.Header{Number: big.NewInt(100)}, nil, false) hashDB1 := witness1.MakeHashDB(diskdb) code1 := []byte("contract1") @@ -309,7 +309,7 @@ func TestCodePersistenceAcrossWitnesses(t *testing.T) { rawdb.WriteCode(hashDB1, hash1, code1) // Second witness import - verify code1 is still accessible - witness2, _ := NewWitness(&types.Header{Number: big.NewInt(101)}, nil) + witness2, _ := NewWitness(&types.Header{Number: big.NewInt(101)}, nil, false) hashDB2 := witness2.MakeHashDB(diskdb) // Verify code1 is accessible through new hashDB @@ -325,7 +325,7 @@ func TestCodePersistenceAcrossWitnesses(t *testing.T) { rawdb.WriteCode(hashDB2, hash2, code2) // Third witness import - verify both codes are accessible - witness3, _ := NewWitness(&types.Header{Number: big.NewInt(102)}, nil) + witness3, _ := NewWitness(&types.Header{Number: big.NewInt(102)}, nil, false) hashDB3 := witness3.MakeHashDB(diskdb) for hash, expectedCode := range map[common.Hash][]byte{hash1: code1, hash2: code2} { diff --git a/core/stateless/encoding.go b/core/stateless/encoding.go index e955b9c962..8ccac1dfc3 100644 --- a/core/stateless/encoding.go +++ b/core/stateless/encoding.go @@ -65,8 +65,8 @@ func (w *Witness) ToExtWitness() *ExtWitness { return ext } -// fromExtWitness converts the ExtWitness format into our internal representation. -func (w *Witness) fromExtWitness(ext *ExtWitness) error { +// FromExtWitness converts the ExtWitness format into our internal representation. +func (w *Witness) FromExtWitness(ext *ExtWitness) error { w.context = ext.Context w.Headers = ext.Headers @@ -131,5 +131,5 @@ func (w *Witness) DecodeRLP(s *rlp.Stream) error { if err := rlp.DecodeBytes(raw, &ext); err != nil { return err } - return w.fromExtWitness(&ext) + return w.FromExtWitness(&ext) } diff --git a/core/stateless/stats.go b/core/stateless/stats.go index 7f4473a67c..8c05b23d37 100644 --- a/core/stateless/stats.go +++ b/core/stateless/stats.go @@ -54,6 +54,13 @@ func NewWitnessStats() *WitnessStats { } } +func (s *WitnessStats) copy() *WitnessStats { + return &WitnessStats{ + accountTrie: s.accountTrie.Copy(), + storageTrie: s.storageTrie.Copy(), + } +} + func (s *WitnessStats) init() { if s.accountTrie == nil { s.accountTrie = trie.NewLevelStats() diff --git a/core/stateless/witness.go b/core/stateless/witness.go index c418b0d129..5529cc33f0 100644 --- a/core/stateless/witness.go +++ b/core/stateless/witness.go @@ -80,12 +80,13 @@ type Witness struct { Codes map[string]struct{} // Set of bytecodes ran or accessed State map[string]struct{} // Set of MPT state trie nodes (account and storage together) - chain HeaderReader // Chain reader to convert block hash ops to header proofs - lock sync.RWMutex // Lock to allow concurrent state insertions + chain HeaderReader // Chain reader to convert block hash ops to header proofs + stats *WitnessStats // Optional statistics collector + lock sync.RWMutex // Lock to allow concurrent state insertions } // NewWitness creates an empty witness ready for population. -func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { +func NewWitness(context *types.Header, chain HeaderReader, enableStats bool) (*Witness, error) { // When building witnesses, retrieve the parent header, which will *always* // be included to act as a trustless pre-root hash container var headers []*types.Header @@ -97,13 +98,17 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { headers = append(headers, parent) } // Create the witness with a reconstructed gutted out block - return &Witness{ + w := &Witness{ context: context, Headers: headers, Codes: make(map[string]struct{}), State: make(map[string]struct{}), chain: chain, - }, nil + } + if enableStats { + w.stats = NewWitnessStats() + } + return w, nil } // AddBlockHash adds a "blockhash" to the witness with the designated offset from @@ -135,8 +140,11 @@ func (w *Witness) AddCode(code []byte) { w.Codes[string(code)] = struct{}{} } -// AddState inserts a batch of MPT trie nodes into the witness. -func (w *Witness) AddState(nodes map[string][]byte) { +// AddState inserts a batch of MPT trie nodes into the witness. The owner +// identifies which trie the nodes belong to: the zero hash for the account +// trie, or the hashed address for a storage trie. This is used for optional +// statistics collection. +func (w *Witness) AddState(nodes map[string][]byte, owner common.Hash) { if len(nodes) == 0 { return } @@ -146,6 +154,17 @@ func (w *Witness) AddState(nodes map[string][]byte) { for _, value := range nodes { w.State[string(value)] = struct{}{} } + if w.stats != nil { + w.stats.Add(nodes, owner) + } +} + +// ReportMetrics reports the collected statistics to the global metrics registry. +func (w *Witness) ReportMetrics(blockNumber uint64) { + if w.stats == nil { + return + } + w.stats.ReportMetrics(blockNumber) } func (w *Witness) AddKey() { @@ -163,6 +182,9 @@ func (w *Witness) Copy() *Witness { State: maps.Clone(w.State), chain: w.chain, } + if w.stats != nil { + cpy.stats = w.stats.copy() + } if w.context != nil { cpy.context = types.CopyHeader(w.context) } diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 417a3fa598..0ebd640974 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -303,7 +303,7 @@ const ( // this generates an increase in gas. There is at most one of such gas change per transaction. GasChangeTxRefunds GasChangeReason = 3 // GasChangeTxLeftOverReturned is the amount of gas left over at the end of transaction's execution that will be returned - // to the chain. This change will always be a negative change as we "drain" left over gas towards 0. If there was no gas + // to the account. This change will always be a negative change as we "drain" left over gas towards 0. If there was no gas // left at the end of execution, no such even will be emitted. The returned gas's value in Wei is returned to caller. // There is at most one of such gas change per transaction. GasChangeTxLeftOverReturned GasChangeReason = 4 @@ -371,7 +371,7 @@ const ( // NonceChangeNewContract is the nonce change of a newly created contract. NonceChangeNewContract NonceChangeReason = 4 - // NonceChangeTransaction is the nonce change due to a EIP-7702 authorization. + // NonceChangeAuthorization is the nonce change due to a EIP-7702 authorization. NonceChangeAuthorization NonceChangeReason = 5 // NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure. diff --git a/core/tracing/journal.go b/core/tracing/journal.go index 62a70d6c27..560c937115 100644 --- a/core/tracing/journal.go +++ b/core/tracing/journal.go @@ -155,10 +155,18 @@ func (j *journal) OnBalanceChange(addr common.Address, prev, new *big.Int, reaso } func (j *journal) OnNonceChangeV2(addr common.Address, prev, new uint64, reason NonceChangeReason) { - // When a contract is created, the nonce of the creator is incremented. - // This change is not reverted when the creation fails. - if reason != NonceChangeContractCreator { - j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new}) + j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new}) + if reason == NonceChangeContractCreator { + // When a contract is created via CREATE/CREATE2, the creator's nonce is + // incremented. The EVM does not revert this when the CREATE frame itself + // fails (the nonce change happens before the EVM snapshot). However, if + // a parent frame reverts, the nonce must be reverted along with everything + // else. + // + // To achieve this, advance the current frame's revision point past this + // entry. The CREATE frame's revert won't touch it (it's below the revision), + // but a parent frame's revert will (it's above the parent's revision). + j.revisions[len(j.revisions)-1] = len(j.entries) } if j.hooks.OnNonceChangeV2 != nil { j.hooks.OnNonceChangeV2(addr, prev, new, reason) diff --git a/core/tracing/journal_test.go b/core/tracing/journal_test.go index e00447f5f3..488d192502 100644 --- a/core/tracing/journal_test.go +++ b/core/tracing/journal_test.go @@ -219,6 +219,42 @@ func TestNonceIncOnCreate(t *testing.T) { } } +// TestNonceIncOnCreateParentReverts checks that the creator's nonce increment +// from CREATE survives the CREATE frame's own revert but is properly reverted +// when the parent call frame reverts. +func TestNonceIncOnCreateParentReverts(t *testing.T) { + const opCREATE = 0xf0 + + tr := &testTracer{t: t} + wr, err := WrapWithJournal(&Hooks{OnNonceChange: tr.OnNonceChange}) + if err != nil { + t.Fatalf("failed to wrap test tracer: %v", err) + } + + addr := common.HexToAddress("0x1234") + { + // Parent call frame + wr.OnEnter(0, 0, addr, addr, nil, 1000, big.NewInt(0)) + { + // CREATE frame — creator nonce incremented, then CREATE reverts + wr.OnEnter(1, opCREATE, addr, addr, nil, 1000, big.NewInt(0)) + wr.OnNonceChangeV2(addr, 0, 1, NonceChangeContractCreator) + wr.OnExit(1, nil, 100, errors.New("revert"), true) + } + // After CREATE reverts, nonce should still be 1 + if tr.nonce != 1 { + t.Fatalf("nonce after CREATE revert: got %v, want 1", tr.nonce) + } + // Parent frame also reverts + wr.OnExit(0, nil, 150, errors.New("revert"), true) + } + + // After parent reverts, nonce should be back to 0 + if tr.nonce != 0 { + t.Fatalf("nonce after parent revert: got %v, want 0", tr.nonce) + } +} + func TestOnNonceChangeV2(t *testing.T) { tr := &testTracer{t: t} wr, err := WrapWithJournal(&Hooks{OnNonceChangeV2: tr.OnNonceChangeV2}) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 8ba714ca2a..a09e06d561 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1392,11 +1392,7 @@ func (pool *LegacyPool) Status(hash common.Hash) txpool.TxStatus { // Get returns a transaction if it is contained in the pool and nil otherwise. func (pool *LegacyPool) Get(hash common.Hash) *types.Transaction { - tx := pool.get(hash) - if tx == nil { - return nil - } - return tx + return pool.get(hash) } // get returns a transaction if it is contained in the pool and nil otherwise. @@ -1818,7 +1814,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T // promote all promotable transactions promoted := make([]*types.Transaction, 0, len(promotable)) for _, tx := range promotable { - from, _ := pool.signer.Sender(tx) + from, _ := types.Sender(pool.signer, tx) // already validated if pool.promoteTx(from, tx.Hash(), tx) { promoted = append(promoted, tx) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index e93acff6c6..4d8d7b32f9 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -88,8 +89,10 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return fmt.Errorf("%w: type %d rejected, pool not yet in Prague", core.ErrTxTypeNotSupported, tx.Type()) } // Check whether the init code size has been exceeded - if rules.IsShanghai && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { - return fmt.Errorf("%w: code size %v, limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize) + if tx.To() == nil { + if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(tx.Data()))); err != nil { + return err + } } // Bor: EIP-7825 at Madhugiri HF block if (rules.IsOsaka || rules.IsMadhugiri) && tx.Gas() > params.MaxTxGas { diff --git a/core/types/log.go b/core/types/log.go index f0e6a3a745..487ca57b5a 100644 --- a/core/types/log.go +++ b/core/types/log.go @@ -19,6 +19,8 @@ package types import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) //go:generate go run ../../rlp/rlpgen -type Log -out gen_log_rlp.go @@ -62,3 +64,32 @@ type logMarshaling struct { BlockTimestamp hexutil.Uint64 Index hexutil.Uint } + +// EthTransferLog creates and ETH transfer log according to EIP-7708. +// Specification: https://eips.ethereum.org/EIPS/eip-7708 +func EthTransferLog(from, to common.Address, amount *uint256.Int) *Log { + amount32 := amount.Bytes32() + return &Log{ + Address: params.SystemAddress, + Topics: []common.Hash{ + params.EthTransferLogEvent, + common.BytesToHash(from.Bytes()), + common.BytesToHash(to.Bytes()), + }, + Data: amount32[:], + } +} + +// EthBurnLog creates an ETH burn log according to EIP-7708. +// Specification: https://eips.ethereum.org/EIPS/eip-7708 +func EthBurnLog(from common.Address, amount *uint256.Int) *Log { + amount32 := amount.Bytes32() + return &Log{ + Address: params.SystemAddress, + Topics: []common.Hash{ + params.EthBurnLogEvent, + common.BytesToHash(from.Bytes()), + }, + Data: amount32[:], + } +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 182476d735..6eef7ac286 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -335,11 +335,15 @@ func (tx *Transaction) To() *common.Address { // Cost returns (gas * gasPrice) + (blobGas * blobGasPrice) + value. func (tx *Transaction) Cost() *big.Int { - total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) - if tx.Type() == BlobTxType { - total.Add(total, new(big.Int).Mul(tx.BlobGasFeeCap(), new(big.Int).SetUint64(tx.BlobGas()))) + // Avoid allocating copies via tx.GasPrice()/tx.Value(); use inner values directly. + total := new(big.Int).SetUint64(tx.inner.gas()) + total.Mul(total, tx.inner.gasPrice()) + if blobtx, ok := tx.inner.(*BlobTx); ok { + tmp := new(big.Int).SetUint64(blobtx.blobGas()) + tmp.Mul(tmp, blobtx.BlobFeeCap.ToBig()) + total.Add(total, tmp) } - total.Add(total, tx.Value()) + total.Add(total, tx.inner.value()) return total } diff --git a/core/v1_differential_test.go b/core/v1_differential_test.go index 4a02ebc668..45907f7b9f 100644 --- a/core/v1_differential_test.go +++ b/core/v1_differential_test.go @@ -178,7 +178,7 @@ func runV1Serial(t *testing.T, sc v1Scenario, chainConfig *params.ChainConfig) c } evm := vm.NewEVM(blockCtx, sdb, chainConfig, vm.Config{}) evm.SetTxContext(NewEVMTxContext(msg)) - result, err := ApplyMessage(evm, msg, new(GasPool).AddGas(blockCtx.GasLimit)) + result, err := ApplyMessage(evm, msg, NewGasPool(blockCtx.GasLimit)) if err != nil { t.Fatalf("tx %d apply: %v", i, err) } diff --git a/core/v2_blockstm_test.go b/core/v2_blockstm_test.go index e7a6d37f39..0746e91edd 100644 --- a/core/v2_blockstm_test.go +++ b/core/v2_blockstm_test.go @@ -347,9 +347,8 @@ func TestV2_SelfDestructTransferLog_MispairsWithSerial(t *testing.T) { serialDB, _ := state.New(root, state.NewDatabase(tdb, nil)) serialDB.SetTxContext(tx.Hash(), 0) serialEVM := vm.NewEVM(blockCtx, serialDB, &cfg, vm.Config{}) - usedGas := uint64(0) - serialReceipt, err := ApplyTransactionWithEVM(msg, new(GasPool).AddGas(blockCtx.GasLimit), - serialDB, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, &usedGas, serialEVM) + serialReceipt, err := ApplyTransactionWithEVM(msg, NewGasPool(blockCtx.GasLimit), + serialDB, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, serialEVM) if err != nil { t.Fatalf("serial ApplyTransactionWithEVM: %v", err) } diff --git a/core/v2_metamorphic_parity_test.go b/core/v2_metamorphic_parity_test.go index 70be8be9b5..d5c692c845 100644 --- a/core/v2_metamorphic_parity_test.go +++ b/core/v2_metamorphic_parity_test.go @@ -113,13 +113,12 @@ func TestV2SerialParity_MetamorphicCreate2(t *testing.T) { // Serial. serialDB, _ := state.New(root, state.NewDatabase(tdb, nil)) - gp := new(GasPool).AddGas(blockCtx.GasLimit) - var usedGas uint64 + gp := NewGasPool(blockCtx.GasLimit) serialEVM := vm.NewEVM(blockCtx, serialDB, &cfg, vm.Config{}) for j, tx := range txs { serialDB.SetTxContext(tx.Hash(), j) if _, err := ApplyTransactionWithEVM(msgs[j], gp, serialDB, blockCtx.BlockNumber, - common.Hash{}, blockCtx.Time, tx, &usedGas, serialEVM); err != nil { + common.Hash{}, blockCtx.Time, tx, serialEVM); err != nil { t.Fatalf("iter %d serial tx %d: %v", i, j, err) } } diff --git a/core/v2_pre_exec_system_call_test.go b/core/v2_pre_exec_system_call_test.go index 709d4f4cd4..44dc5fd1ce 100644 --- a/core/v2_pre_exec_system_call_test.go +++ b/core/v2_pre_exec_system_call_test.go @@ -101,7 +101,7 @@ func runEIP4788Roundtrip(t *testing.T, useV2 bool) { ProcessBeaconBlockRoot(beaconRoot, evm) msg, _ := TransactionToMessage(tx, signer, blockCtx.BaseFee) evm.SetTxContext(NewEVMTxContext(msg)) - gp := new(GasPool).AddGas(blockCtx.GasLimit) + gp := NewGasPool(blockCtx.GasLimit) if _, err := ApplyMessage(evm, msg, gp); err != nil { t.Fatalf("ApplyMessage: %v", err) } diff --git a/core/v2_selfdestruct_self_beneficiary_test.go b/core/v2_selfdestruct_self_beneficiary_test.go index ecff2d120e..f72ea2dd03 100644 --- a/core/v2_selfdestruct_self_beneficiary_test.go +++ b/core/v2_selfdestruct_self_beneficiary_test.go @@ -107,7 +107,7 @@ func runSelfDestructSelfBeneficiary(t *testing.T, useV2 bool) { evm := vm.NewEVM(blockCtx, statedb, &cfg, vm.Config{}) msg, _ := TransactionToMessage(tx, signer, blockCtx.BaseFee) evm.SetTxContext(NewEVMTxContext(msg)) - gp := new(GasPool).AddGas(blockCtx.GasLimit) + gp := NewGasPool(blockCtx.GasLimit) if _, err := ApplyMessage(evm, msg, gp); err != nil { t.Fatalf("ApplyMessage: %v", err) } diff --git a/core/v2_serial_parity_fuzz_test.go b/core/v2_serial_parity_fuzz_test.go index e367b1bd64..8f72e20840 100644 --- a/core/v2_serial_parity_fuzz_test.go +++ b/core/v2_serial_parity_fuzz_test.go @@ -66,8 +66,7 @@ func runSerial(t testing.TB, tdb *triedb.Database, root common.Hash, txs []*type if err != nil { t.Fatal(err) } - gp := new(GasPool).AddGas(blockCtx.GasLimit) - var usedGas uint64 + gp := NewGasPool(blockCtx.GasLimit) receipts := make(types.Receipts, 0, len(txs)) for i, tx := range txs { sdb.SetTxContext(tx.Hash(), i) @@ -76,7 +75,7 @@ func runSerial(t testing.TB, tdb *triedb.Database, root common.Hash, txs []*type // bounded values against huge balances), so an apply error is a // generator bug, not an accepted outcome. EVM-level failures // (revert, OOG) still produce a status-failed receipt. - receipt, err := ApplyTransactionWithEVM(msgs[i], gp, sdb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, &usedGas, evm) + receipt, err := ApplyTransactionWithEVM(msgs[i], gp, sdb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, evm) if err != nil { t.Fatalf("serial apply tx %d: %v", i, err) } @@ -502,12 +501,11 @@ func TestMetamorphicHarnessSemantics(t *testing.T) { if err != nil { t.Fatal(err) } - gp := new(GasPool).AddGas(blockCtx.GasLimit) - var usedGas uint64 + gp := NewGasPool(blockCtx.GasLimit) for i, tx := range txs { sdb.SetTxContext(tx.Hash(), i) evm := vm.NewEVM(blockCtx, sdb, fuzzChainConfig, vm.Config{}) - if _, err := ApplyTransactionWithEVM(msgs[i], gp, sdb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, &usedGas, evm); err != nil { + if _, err := ApplyTransactionWithEVM(msgs[i], gp, sdb, blockCtx.BlockNumber, common.Hash{}, blockCtx.Time, tx, evm); err != nil { t.Fatalf("tx %d: %v", i, err) } } diff --git a/core/v2_witness_regen_test.go b/core/v2_witness_regen_test.go index a3bc9e96bd..9f1848b938 100644 --- a/core/v2_witness_regen_test.go +++ b/core/v2_witness_regen_test.go @@ -50,7 +50,7 @@ func witnessRegenRoundTrip(pb *preparedBlock, diskdb ethdb.Database, config *par } hc := &benchHeaderChain{config: config, chainDb: pb.memdb, headerCache: pb.headerCache, engine: engine} - w2, err := stateless.NewWitness(pb.block.Header(), hc) + w2, err := stateless.NewWitness(pb.block.Header(), hc, false) if err != nil { return fmt.Errorf("new witness: %w", err) } diff --git a/core/vm/common.go b/core/vm/common.go index 5063e1a5d2..7cd1016b05 100644 --- a/core/vm/common.go +++ b/core/vm/common.go @@ -17,13 +17,44 @@ package vm import ( + "fmt" "math" "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" ) +// CheckMaxInitCodeSize checks the size of contract initcode against the protocol-defined limit. +func CheckMaxInitCodeSize(rules *params.Rules, size uint64) error { + if rules.IsAmsterdam { + if size > params.MaxInitCodeSizeAmsterdam { + return fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, size, params.MaxInitCodeSizeAmsterdam) + } + } else if rules.IsShanghai { + if size > params.MaxInitCodeSize { + return fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, size, params.MaxInitCodeSize) + } + } + + return nil +} + +// CheckMaxCodeSize checks the size of contract code against the protocol-defined limit. +func CheckMaxCodeSize(rules *params.Rules, size uint64) error { + if rules.IsAmsterdam { + if size > params.MaxCodeSizeAmsterdam { + return fmt.Errorf("%w: code size %v limit %v", ErrMaxCodeSizeExceeded, size, params.MaxCodeSizeAmsterdam) + } + } else if rules.IsEIP158 { + if size > params.MaxCodeSize { + return fmt.Errorf("%w: code size %v limit %v", ErrMaxCodeSizeExceeded, size, params.MaxCodeSize) + } + } + return nil +} + // calcMemSize64 calculates the required memory size, and returns // the size and whether the result overflowed uint64 func calcMemSize64(off, l *uint256.Int) (uint64, bool) { diff --git a/core/vm/dispatch_bench_test.go b/core/vm/dispatch_bench_test.go index 616ccac6ef..143c2e4117 100644 --- a/core/vm/dispatch_bench_test.go +++ b/core/vm/dispatch_bench_test.go @@ -59,7 +59,7 @@ func benchSnailtracer(b *testing.B, switchDispatch bool) { bctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, GetHash: func(uint64) common.Hash { return common.Hash{} }, BlockNumber: big.NewInt(1), Time: 1, diff --git a/core/vm/dispatch_test.go b/core/vm/dispatch_test.go index d23d90badf..2cbd7ab119 100644 --- a/core/vm/dispatch_test.go +++ b/core/vm/dispatch_test.go @@ -181,7 +181,7 @@ func execPathResultWithConfig( bctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, GetHash: func(n uint64) common.Hash { return common.BigToHash(new(big.Int).SetUint64(n + 0x1000)) }, Coinbase: coinbase, BlockNumber: big.NewInt(11), @@ -1537,7 +1537,7 @@ func makeEVM(code []byte, gas uint64, switchDispatch bool) (*EVM, common.Address bctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, GetHash: func(n uint64) common.Hash { return common.BigToHash(new(big.Int).SetUint64(n + 0x1000)) }, Coinbase: coinbase, BlockNumber: big.NewInt(11), diff --git a/core/vm/evm.go b/core/vm/evm.go index 8c2bcc2c6f..4e044cb24a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -37,7 +37,7 @@ type ( // CanTransferFunc is the signature of a transfer guard function CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool // TransferFunc is the signature of a transfer function - TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int) + TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) // GetHashFunc returns the n'th block hash in the blockchain // and is used by the BLOCKHASH EVM op code. GetHashFunc func(uint64) common.Hash @@ -374,8 +374,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. if !syscall { - evm.Context.Transfer(evm.StateDB, caller, addr, value) + evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules) } + if isPrecompile { ret, gas, err = evm.runPrecompile(p, addr, input, gas) } else { @@ -654,7 +655,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui } gas = gas - consumed } - evm.Context.Transfer(evm.StateDB, caller, address, value) + evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) // 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. @@ -685,14 +686,16 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } // Check whether the max code size has been exceeded, assign err if the case. - if evm.chainRules.IsEIP158 { - if evm.chainConfig.Bor != nil && evm.chainConfig.Bor.IsAhmedabad(evm.Context.BlockNumber) { - if len(ret) > params.MaxCodeSizePostAhmedabad { - err = ErrMaxCodeSizeExceeded - } - } else if len(ret) > params.MaxCodeSize { + // Ahmedabad raises the cap to 32KB on Bor networks and takes precedence; off Bor + // (or pre-Ahmedabad) the upstream helper applies, whose Amsterdam branch is dormant + // while AmsterdamBlock is nil. Bor assigns err and keeps going instead of returning + // early, so the deployment gas is still charged. + if evm.chainConfig.Bor != nil && evm.chainConfig.Bor.IsAhmedabad(evm.Context.BlockNumber) { + if evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSizePostAhmedabad { err = ErrMaxCodeSizeExceeded } + } else if sizeErr := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); sizeErr != nil { + err = sizeErr } // Reject code starting with 0xEF if EIP-3541 is enabled. diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index dbf9cb7be2..058953d971 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -18,7 +18,6 @@ package vm import ( "errors" - "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -339,10 +338,10 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m if overflow { return 0, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { - return 0, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return 0, err } - // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow + // Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow moreGas := params.InitCodeWordGas * ((size + 31) / 32) if gas, overflow = math.SafeAdd(gas, moreGas); overflow { return 0, ErrGasUintOverflow @@ -360,10 +359,10 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if overflow { return 0, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { - return 0, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return 0, err } - // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow + // Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow moreGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32) if gas, overflow = math.SafeAdd(gas, moreGas); overflow { return 0, ErrGasUintOverflow diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index deadbdcff6..49c16a56c5 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -96,7 +96,7 @@ func TestEIP2200(t *testing.T) { vmctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) @@ -151,16 +151,20 @@ func TestCreateGas(t *testing.T) { vmctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, BlockNumber: big.NewInt(0), } config := Config{} + chainConfig := params.AllEthashProtocolChanges if tt.eip3860 { config.ExtraEips = []int{3860} + vmctx.Random = new(common.Hash) + + chainConfig = params.MergedTestChainConfig } - evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, config) + evm := NewEVM(vmctx, statedb, chainConfig, config) var startGas = uint64(testGas) ret, gas, err := evm.Call(common.Address{}, address, nil, startGas, new(uint256.Int)) if err != nil { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 12fd8e8cdd..ecd8001528 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -988,7 +988,20 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro balance := evm.StateDB.GetBalance(scope.Contract.Address()) evm.StateDB.SubBalance(scope.Contract.Address(), balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct) - evm.StateDB.SelfDestruct6780(scope.Contract.Address()) + // Bor keeps the pre-#32919 selfdestruct shape, so the EIP-7708 "contract is new + // and will actually be deleted" signal comes from SelfDestruct6780's second + // return value rather than upstream's StateDB.IsNewContract. + _, deleted := evm.StateDB.SelfDestruct6780(scope.Contract.Address()) + + if evm.chainRules.IsAmsterdam && !balance.IsZero() { + this := scope.Contract.Address() + if this != beneficiary.Bytes20() { + evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary.Bytes20(), balance)) + } else if deleted { + evm.StateDB.AddLog(types.EthBurnLog(this, balance)) + } + } + if tracer := evm.Config.Tracer; tracer != nil { if tracer.OnEnter != nil { tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig()) @@ -1001,24 +1014,34 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro return nil, errStopToken } +// decodeSingle decodes the immediate operand of a backward-compatible DUPN or SWAPN instruction (EIP-8024) +// https://eips.ethereum.org/EIPS/eip-8024 func decodeSingle(x byte) int { - if x <= 90 { - return int(x) + 17 - } - return int(x) - 20 -} - + // Depths 1-16 are already covered by the legacy opcodes. The forbidden byte range [91, 127] removes + // 37 values from the 256 possible immediates, leaving 219 usable values, so this encoding covers depths + // 17 through 235. The immediate is encoded as (x + 111) % 256, where 111 is chosen so that these values + // avoid the forbidden range. Decoding is simply the modular inverse (i.e. 111+145=256). + return (int(x) + 145) % 256 +} + +// decodePair decodes the immediate operand of a backward-compatible EXCHANGE +// instruction (EIP-8024) into stack indices (n, m) where 1 <= n < m +// and n + m <= 30. The forbidden byte range [82, 127] removes 46 values from +// the 256 possible immediates, leaving exactly 210 usable bytes. +// https://eips.ethereum.org/EIPS/eip-8024 func decodePair(x byte) (int, int) { - var k int - if x <= 79 { - k = int(x) - } else { - k = int(x) - 48 - } + // XOR with 143 remaps the forbidden bytes [82, 127] to an unused corner + // of the 16x16 grid below. + k := int(x ^ 143) + // Split into row q and column r of a 16x16 grid. The 210 valid pairs + // occupy two triangles within this grid. q, r := k/16, k%16 + // Upper triangle (q < r): pairs where m <= 16, encoded directly as + // (q+1, r+1). if q < r { return q + 1, r + 1 } + // Lower triangle: pairs where m > 16, recovered as (r+1, 29-q). return r + 1, 29 - q } @@ -1089,8 +1112,8 @@ func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } // This range is excluded both to preserve compatibility with existing opcodes - // and to keep decode_pair’s 16-aligned arithmetic mapping valid (0–79, 128–255). - if x > 79 && x < 128 { + // and to keep decode_pair’s 16-aligned arithmetic mapping valid (0–81, 128–255). + if x > 81 && x < 128 { return nil, &ErrInvalidOpCode{opcode: OpCode(x)} } n, m := decodePair(x) @@ -1133,9 +1156,6 @@ func makeLog(size int) executionFunc { Address: scope.Contract.Address(), Topics: topics, Data: d, - // This is a non-consensus field, but assigned here because - // core/state doesn't know the current block number. - BlockNumber: evm.Context.BlockNumber.Uint64(), }) return nil, nil diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index ebdedf987c..4a62125d9c 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -1093,16 +1093,7 @@ func TestEIP8024_Execution(t *testing.T) { }{ { name: "DUPN", - codeHex: "60016000808080808080808080808080808080e600", - wantVals: []uint64{ - 1, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 1, - }, - }, - { - name: "DUPN_MISSING_IMMEDIATE", - codeHex: "60016000808080808080808080808080808080e6", + codeHex: "60016000808080808080808080808080808080e680", wantVals: []uint64{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -1111,7 +1102,7 @@ func TestEIP8024_Execution(t *testing.T) { }, { name: "SWAPN", - codeHex: "600160008080808080808080808080808080806002e700", + codeHex: "600160008080808080808080808080808080806002e780", wantVals: []uint64{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -1119,22 +1110,23 @@ func TestEIP8024_Execution(t *testing.T) { }, }, { - name: "SWAPN_MISSING_IMMEDIATE", - codeHex: "600160008080808080808080808080808080806002e7", + name: "EXCHANGE_MISSING_IMMEDIATE", + codeHex: "600260008080808080600160008080808080808080e8", wantVals: []uint64{ - 1, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, // 10th from top + 0, 0, 0, 0, 0, 0, + 1, // bottom }, }, { name: "EXCHANGE", - codeHex: "600060016002e801", + codeHex: "600060016002e88e", wantVals: []uint64{2, 0, 1}, }, { - name: "EXCHANGE_MISSING_IMMEDIATE", - codeHex: "600060006000600060006000600060006000600060006000600060006000600060006000600060006000600060006000600060006000600060016002e8", + name: "EXCHANGE", + codeHex: "600080808080808080808080808080808080808080808080808080808060016002e88f", wantVals: []uint64{ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -1148,68 +1140,31 @@ func TestEIP8024_Execution(t *testing.T) { wantOpcode: SWAPN, }, { - name: "JUMP over INVALID_DUPN", + name: "JUMP_OVER_INVALID_DUPN", codeHex: "600456e65b", wantErr: nil, }, { - name: "UNDERFLOW_DUPN_1", - codeHex: "6000808080808080808080808080808080e600", - wantErr: &ErrStackUnderflow{}, - wantOpcode: DUPN, - }, - // Additional test cases - { - name: "INVALID_DUPN_LOW", - codeHex: "e65b", - wantErr: &ErrInvalidOpCode{}, - wantOpcode: DUPN, - }, - { - name: "INVALID_EXCHANGE_LOW", - codeHex: "e850", - wantErr: &ErrInvalidOpCode{}, - wantOpcode: EXCHANGE, - }, - { - name: "INVALID_DUPN_HIGH", - codeHex: "e67f", - wantErr: &ErrInvalidOpCode{}, - wantOpcode: DUPN, - }, - { - name: "INVALID_SWAPN_HIGH", - codeHex: "e77f", - wantErr: &ErrInvalidOpCode{}, - wantOpcode: SWAPN, + name: "EXCHANGE", + codeHex: "60008080e88e15", + wantVals: []uint64{1, 0, 0}, }, { - name: "INVALID_EXCHANGE_HIGH", - codeHex: "e87f", + name: "INVALID_EXCHANGE", + codeHex: "e852", wantErr: &ErrInvalidOpCode{}, wantOpcode: EXCHANGE, }, { - name: "UNDERFLOW_DUPN_2", - codeHex: "5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5fe600", // (n=17, need 17 items, have 16) + name: "UNDERFLOW_DUPN", + codeHex: "6000808080808080808080808080808080e680", wantErr: &ErrStackUnderflow{}, wantOpcode: DUPN, }, - { - name: "UNDERFLOW_SWAPN", - codeHex: "5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5fe700", // (n=17, need 18 items, have 17) - wantErr: &ErrStackUnderflow{}, - wantOpcode: SWAPN, - }, - { - name: "UNDERFLOW_EXCHANGE", - codeHex: "60016002e801", // (n,m)=(1,2), need 3 items, have 2 - wantErr: &ErrStackUnderflow{}, - wantOpcode: EXCHANGE, - }, + // Additional test cases { name: "PC_INCREMENT", - codeHex: "600060006000e80115", + codeHex: "600060006000e88e15", wantVals: []uint64{1, 0, 0}, }, } diff --git a/core/vm/interface.go b/core/vm/interface.go index a996de3e61..a58547463e 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -91,6 +91,7 @@ type StateDB interface { Snapshot() int AddLog(*types.Log) + EmitLogsForBurnAccounts() AddPreimage(common.Hash, []byte) // RecordTransfer records a transfer for deferred log creation in parallel mode. diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 1595071e99..375da19fb6 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -41,7 +41,7 @@ var loopInterruptTests = []string{ func TestLoopInterrupt(t *testing.T) { address := common.BytesToAddress([]byte("contract")) vmctx := BlockContext{ - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, } for i, tt := range loopInterruptTests { diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index fdde29f73b..066cbb9eec 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -38,6 +38,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) { return newPragueInstructionSet(), nil case rules.IsMadhugiri: return newPragueInstructionSet(), nil + case rules.IsAmsterdam: + return newAmsterdamInstructionSet(), nil case rules.IsOsaka: return newOsakaInstructionSet(), nil case rules.IsVerkle: diff --git a/docs/upstream-merges/v1.17.4/fork-register.md b/docs/upstream-merges/v1.17.4/fork-register.md index 844b95468f..dbdd403269 100644 --- a/docs/upstream-merges/v1.17.4/fork-register.md +++ b/docs/upstream-merges/v1.17.4/fork-register.md @@ -21,6 +21,11 @@ gate-guarded (not ungated / unconditional). | Amsterdam VM fork (gate) | #33742/#33589/#33928 | 14–15 (v1.17.1) | `params/config.go` | **wired block-based-nil like Osaka**: added `AmsterdamBlock *big.Int` (nil on every preset), `IsAmsterdam(num) = IsLondon(num) && isBlockForked(AmsterdamBlock, num)`, `Rules.IsAmsterdam` + `Rules()` population, and `IsAmsterdam` in `TestReinforceMultiClientPreCompilesTest`'s expected list. Corrects the v1.17.0 drop of upstream's `AmsterdamTime`/`IsAmsterdam`. | **enable-by-block** — dormant | yes — `AmsterdamBlock` nil on all presets → `isBlockForked(nil,·)` false → `IsAmsterdam` false everywhere. Enabling = set `AmsterdamBlock` on the target preset(s) (same as Osaka/Prague). | | EIP-7843 SLOTNUM (opcode `0x4b`) | #33589 (`f811bfe4f`) | 15 (v1.17.1 1/2) | `core/vm/{opcodes,eips,jump_table,evm}.go`, `core/{evm,genesis}.go`, `core/types/block.go`, `miner/worker.go`, `consensus/beacon/consensus.go`, `core/state_processor_test.go` | opcode + `opSlotNum` + `enable7843` wired into `newAmsterdamInstructionSet`, dispatched via `case evm.chainRules.IsAmsterdam`; header `SlotNumber *uint64 rlp:"optional"` populated under `IsAmsterdam(num)` gate in genesis + miner `makeHeader`; validated under the gate in `consensus/beacon`. All gated on the (dormant) `IsAmsterdam`. | **enable-by-block** — dormant | yes — `IsAmsterdam` false on every preset → `amsterdamInstructionSet` never selected (SLOTNUM absent from active jump tables), header `SlotNumber` never set (nil → RLP unchanged, core/types tests pass), validation takes the pre-Amsterdam branch. **Caveat: enabling also requires Bor's block producer to supply `genParams.slotNum`** (Bor has no beacon slot — the miner errors "no slot number set post-amsterdam" otherwise). | | EIP-8024 enabled in Amsterdam | #33928 (`2726c9ef9`) | 15 (v1.17.1 2/2) | `core/vm/jump_table.go` | `enable8024` invoked inside `newAmsterdamInstructionSet`, now instantiated + dispatched under the dormant `IsAmsterdam` gate | **enable-by-block** — dormant | yes — reachable only via the Amsterdam instruction set, which is selected only when `IsAmsterdam` (false on all presets). The earlier EIP-8024 rows (batches 4/5/10, via `ExtraEips`) remain separate and also unset. | +| EIP-7778 (block gas accounting without refunds) | #33593 (`6d0dd0886`) | 16 (v1.17.2 1/4) | `core/{gaspool,state_transition,state_processor,chain_makers,state_prefetcher,error}.go`, `core/parallel_state_processor.go`, `miner/worker.go`, `cmd/evm/internal/t8ntool`, `eth/{state_accessor,tracers}`, `internal/ethapi` | `GasPool` becomes a struct; `stateTransition.execute()` branches on `rules.IsAmsterdam` — post-Amsterdam returns `initialGas − peakGasUsed` to the block pool (refund excluded), pre-Amsterdam returns `gasRemaining` (refund included, the historical behavior). Receipt `GasUsed` / `CumulativeGasUsed` semantics are unchanged in both branches. | **enable-by-block** — dormant | yes — `IsAmsterdam` false on every preset → the pre-Amsterdam branch runs and is arithmetically identical to the pre-merge `AddGas(gasRemaining)` path (`gp.Used()` == old `*usedGas`; `gp.CumulativeUsed()` == old receipt `CumulativeGasUsed`). **When Amsterdam is enabled this changes block-level gas accounting** (refunds no longer return to the block gas pool), so it needs its own N-1/N/N+1 boundary tests and Erigon parity. | +| Amsterdam jump-table dispatch in `LookupInstructionSet` | #33947 (`fe3a74e61`) | 16 (v1.17.2 1/4) | `core/vm/jump_table_export.go` | `case rules.IsAmsterdam → newAmsterdamInstructionSet()`, ordered below Bor's Chicago/LisovoPro/Lisovo/MadhugiriPro/Madhugiri cases and above Osaka, matching the `core/vm/evm.go` dispatch wired in batch 15 | **enable-by-block** — dormant | yes — the case is unreachable while `AmsterdamBlock` is nil; the exported lookup is the RPC/tooling twin of the `evm.go` dispatch, so both agree. | +| EIP-8024 branchless normalization + extended EXCHANGE | #33869 (`814edc530`) | 16 (v1.17.2 1/4) | `core/vm/instructions.go`, `core/vm/instructions_test.go` | no gate change — updates the `enable8024` opcode implementations to the latest spec (EIPs PR 11306) | **defer** (unchanged) | yes — reachable only through `enable8024`, invoked only by `newAmsterdamInstructionSet`. | +| EIP-7954 (increase maximum contract size) | #33832 (`95b9a2ed7`) | 17 (v1.17.2 2/4) | `params/protocol_params.go`, `core/vm/{common,evm,gas_table}.go`, `core/state_transition.go`, `core/txpool/validation.go`, `cmd/evm/internal/t8ntool/transaction.go` | `MaxCodeSizeAmsterdam` (32768) / `MaxInitCodeSizeAmsterdam` (65536) + `vm.CheckMaxCodeSize` / `vm.CheckMaxInitCodeSize`, both branching on `rules.IsAmsterdam` | **enable-by-block** — dormant | yes — `IsAmsterdam` false on every preset → the helpers fall through to the pre-existing `IsEIP158` (24576) / `IsShanghai` (49152) limits. **Overlaps a live Bor fork:** Bor's Ahmedabad already sets the same 32768 code cap (`MaxCodeSizePostAhmedabad`, `Bor.IsAhmedabad`, mainnet `62278656` / Amoy `11865856`), and `initNewContract` checks Ahmedabad first, so on Bor the Amsterdam code-size branch is unreachable in practice. **Enable-time decision:** Ahmedabad does *not* raise the initcode cap, so enabling Amsterdam would move initcode 49152 → 65536 — a separate consensus change needing its own N-1/N/N+1 tests and Erigon parity. Bor also keeps its assign-err-and-continue flow in `initNewContract` (deployment gas still charged) versus upstream's early return. | +| EIP-7708 (ETH transfers as logs) | #33645 (`b87340a85`) | 19 (v1.17.2 4/4) | `params/protocol_params.go`, `core/types/log.go`, `core/evm.go`, `core/state_transition.go`, `core/state/{statedb,statedb_hooked,parallel_statedb}.go`, `core/vm/{evm,instructions,interface}.go` | `EthTransferLogEvent` / `EthBurnLogEvent` topic constants + `types.EthTransferLog` / `types.EthBurnLog`; `Transfer`/`TransferFunc` gain `*params.Rules`; emission sites in `core.Transfer`, `core.EthereumTransfer`, `opSelfdestruct6780`, and `StateDB.EmitLogsForBurnAccounts` (called from `stateTransition.execute`), each behind `rules.IsAmsterdam` | **enable-by-block** — dormant | yes — `IsAmsterdam` false on every preset → no system log is ever emitted and receipts are byte-identical to pre-merge. **Three enable-time consequences, all Bor-specific.** (1) **Duplicate transfer logs:** Bor already emits its own `LogTransfer` from the 0x1010 fee address for every `core.Transfer`, so with Amsterdam active a plain value transfer produces *both* that and the EIP-7708 system log — a bloom and receipt-size change, and a product decision to take before enabling. (2) **V1/V2 log order:** the emission is deliberately placed at the same point relative to the balance change on Bor's serial path, its V2 BlockSTM fast path, and `EthereumTransfer`, because V2 defers the 0x1010 log to settlement — any other placement orders the pair differently under V1 vs V2 and diverges the receipt root. (3) **Selfdestruct depends on a declined PR:** upstream's burn/transfer branch reads `StateDB.IsNewContract` from the deferred #32919 rework; Bor instead takes the `wasNewContract` bool that its own `SelfDestruct6780` already returns. If #32919 is ever adopted, that adaptation must be revisited rather than blindly replaced. Needs N-1/N/N+1 boundary tests and Erigon parity at enable time. | ## Notes diff --git a/docs/upstream-merges/v1.17.4/ledger.md b/docs/upstream-merges/v1.17.4/ledger.md index a73b48e02e..2e0a64bb19 100644 --- a/docs/upstream-merges/v1.17.4/ledger.md +++ b/docs/upstream-merges/v1.17.4/ledger.md @@ -541,3 +541,534 @@ Verification (per-batch tier): `go build ./...` rc=0; `go vet` clean on touched copylocks); `go mod tidy` clean (go.mod diff = c-kzg bump + olekukonko removal). `go test` pass: `core/types`, `core/vm` (guards), `core/` (182s), `miner` (196s), `eth/protocols/eth`, `trie`, `core/txpool/blobpool`, `consensus/beacon`. No leftover conflict markers. + +## v1.17.2 batch 1/4 (`00540f946`, plan row 16) — merged `09c784851` — MILESTONE-OPENING + +Branch `ppatil-upstream-v1.17.2` cut from `ppatil-upstream-v1.17.1` @ `dbae0f4a1` (stacked). +20 first-parent commits, 26 conflicted files (24 content + 2 delete/modify), 65 files in the merge +(+441 / −297). + +Adopted: + +- **EIP-7778 block gas accounting without refunds (#33593, `6d0dd0886`)** — the batch's substantive + change. `core.GasPool` goes from `type GasPool uint64` (+`AddGas`/`SetGas`) to a struct + (`remaining`/`initial`/`cumulativeUsed`) with + `NewGasPool`/`ReturnGas`/`Used`/`CumulativeUsed`/`Snapshot`/`Set`; the `usedGas *uint64` + out-parameter is dropped from `ApplyTransaction`, `ApplyTransactionWithEVM` and `MakeReceipt` + (receipts read `gp.CumulativeUsed()`, headers read `gp.Used()`). Took `core/gaspool.go` from + upstream verbatim and ported every Bor call site: `core/state_processor.go` (kept Bor's + `interruptCtx` guard and state-sync/Finalize block), `core/parallel_state_processor.go` (3 + BlockSTM per-task pools — V1/V2 keep their own `totalUsedGas`, so parallel accounting is + untouched), `miner/worker.go` (`gasPool.Set(gp)` snapshot-restore; + `env.header.GasUsed = env.gasPool.Used()` replaces the out-parameter), + `cmd/evm/internal/t8ntool`, `core/state_prefetcher.go`, `eth/{state_accessor,tracers}`, + `internal/ethapi`, `accounts/abi/bind/backends`, `tests/bor/helper.go` + 8 test files. + **Pre-Amsterdam equivalence:** old `SubGas(limit)` + `AddGas(remaining)` and new + `SubGas(limit)` + `ReturnGas(remaining, gasUsed())` have identical net pool delta + (`limit − remaining == gasUsed()`), so `gp.Used()` == the old `*usedGas` and + `gp.CumulativeUsed()` == the old receipt `CumulativeGasUsed`. The refund-excluding branch is + gated on the dormant `rules.IsAmsterdam`. +- **Amsterdam jump table in lookup (#33947, `fe3a74e61`)** — `LookupInstructionSet` dispatches + `case rules.IsAmsterdam → newAmsterdamInstructionSet()`, placed below Bor's + Chicago/LisovoPro/Lisovo/MadhugiriPro/Madhugiri cases and above Osaka to match the + `core/vm/evm.go` order wired in batch 15. Dormant (gate nil). +- **EIP-8024 branchless normalization + extended EXCHANGE (#33869, `814edc530`)** — auto-merged; + reachable only via `enable8024` in `newAmsterdamInstructionSet` → dormant. +- **batch close (#33708, `dd202d428`)** — `defer blockBatch.Close()` added to Bor's diverged + `writeBlockWithState` (which carries the state-sync-log and witness-write blocks); the + `writeHeadBlock` / `reorg` call sites auto-merged. +- **TestProcessVerkle flaky fix (#33971, `ecee64ecd`)** — adopted upstream's `genesisTriedb` + explicit-close plus reuse of the committed block in `GenerateChainWithGenesis`; kept Bor's 2-arg + `genesis.Commit(db, triedb)`. +- **default cache 4096 (#33836, `28dad943f`)** — adopted: `utils.CacheFlag` default 1024→4096 and + the mainnet-only bump block deleted from `cmd/geth/main.go`. Affects only the geth-style binary; + the production `bor server` path keeps its own `CacheConfig.Cache = 1024`. +- **eth_simulateV1 gas cap / MaxUsedGas (#33952, #32789)** — combined Bor's `gasBudget`, `gasCapped` + return and bor-internal-call gas-cap bypass with upstream's `gp`-based accounting + (`header.GasUsed = gp.Used()`, `gp.CumulativeUsed()` into `MakeReceipt`). +- **go-eth-kzg `v1.4.0→v1.5.0` (#33963)** — adopted in the root module; `go mod tidy` reconciled + go.sum. keeper module kept at Bor HEAD. + +Declined / kept-Bor: + +- **prevent state flushing in RPC (#33931, `6d99759f0`)** — **whole feature deferred.** Introduces + `core.ExecuteConfig` threaded through `BlockChain.ProcessBlock`, moves + `StatelessSelfValidation` / `EnableWitnessStats` from `vm.Config` to `BlockChainConfig`, and + rewrites `debug_executionWitness` to run with `WriteState: false`. Bor has forked every target: + the live `ProcessBlock` is the tuple-returning `(block, parent, witness, followupInterrupt)` form + at `core/blockchain.go:857` (upstream's survives only as dead `processBlock`, + `// nolint : unused`), `insertChain` is restructured, and `eth/api_debug.go` has its own + `ExecutionWitness` / `ExecutionWitnessByHash` built on the Bor signature — which returns a statedb + without persisting, so Bor does not have the bug being fixed. Reverted `core/vm/interpreter.go`, + `eth/api_debug.go`, `eth/backend.go`, `internal/web3ext/web3ext.go`, `tests/block_test_util.go` + wholesale, and the #33931 hunks only in `cmd/utils/flags.go` and `core/blockchain.go` (shared with + #33836 / #33708). See needs-wiring.md. +- **miner/stress/main.go (new file, 210 lines, added by #33593)** — Engine-API-driven block-builder + stress harness; needs `AmsterdamTime` (Bor's gate is block-based), `BlobScheduleConfig.Amsterdam` + and `ethconfig.SlowBlockThreshold` (slow-block deferred in v1.17.0). Removed; see needs-wiring.md. +- **eth/fetcher chain-event nil-guard (#33950, `344ce84a4`)** — the subscription it guards doesn't + exist in Bor (it came with the declined #33378). Kept Bor's. +- **otel `1.39→1.40` (#33946) and `golang.org/x/sys 0.39→0.40`** — declined; Bor is already ahead + (otel 1.43.0 / sdk 1.43.0, x/sys 0.45.0), so adopting would be a downgrade. +- version.go — declined the `Patch=2` bump; kept Bor `Patch=0` / `unstable`. +- DU (deleted-at-HEAD / modified-upstream), removed to keep the deletions: + `eth/tracers/internal/tracetest/selfdestruct_state_test.go` (deferred selfdestruct cluster) and + `internal/telemetry/tracesetup/setup.go` (deferred OTel). + +Merge artifacts fixed — upstream hunks that auto-merged into the wrong place in Bor's restructured +files, all caught by build/vet: + +- `miner/worker.go` — #33945's `func (env *environment) discard()` landed as a duplicate of Bor's + own nil-guarded `discard` (redeclared), and its `defer work.discard()` landed inside `newWorker` + where `work` is undefined. Bor's `generateWork` already has `defer work.discard()` and already + starts the prefetcher unconditionally, so **#33945 is converged in Bor**; both artifacts removed. +- `internal/ethapi/simulate.go` — `var withdrawalsHash *common.Hash` was dropped (upstream moved the + declaration a few lines down); restored at Bor's position inside the loop. +- `eth/tracers/api.go` — `traceTx` returned the removed `usedGas`; now holds the pool + (`gp := core.NewGasPool(message.GasLimit)`) and returns `gp.Used()` (same value). +- `core/state_processor.go` — dangling `spanEnd(&err)` left by upstream's telemetry move landing in + Bor's de-telemetried loop. + +Verification (per-batch tier): `go build ./...` rc=0; `go vet ./...` clean bar the two pre-existing +`//nolint` copylocks (`trie/secure_trie.go:88`, `core/parallel_state_processor.go:341`); `gofmt -l` +clean on all 65 changed files; `go mod tidy` clean (go.mod diff = go-eth-kzg bump only). `go test` +pass: `core/` (183s), `miner/...` (197s), `core/vm/...`, `core/types/...`, `core/state/...`, +`internal/ethapi/...`, `eth/tracers/...`, `eth/fetcher` (139s), `eth/`, `consensus/...`, `trie/...`, +`triedb/...`, `ethdb/...`, `core/rawdb/...`. Fork guards green +(`TestReinforceMultiClientPreCompilesTest`, `TestBorHardforkPrecompileContinuity*`, +`TestV2ForkParity`). `cmd/evm` 4 failures (`TestT8n`, `TestEVMTracing`, `TestEvmRun`, +`TestEvmRunRegEx`) proven pre-existing — the identical set fails on a clean worktree at pre-merge +`dbae0f4a1`. No leftover conflict markers. + +## v1.17.2 batch 2/4 (`77e7e5ad1`, plan row 17) — merged `0a83ed542` + +20 first-parent commits, 23 conflicted files (21 content + 2 delete/modify), 45 files in the merge +(+920 / −177). + +Adopted: + +- **EIP-7954 increase maximum contract size (#33832, `95b9a2ed7`)** — adds + `params.MaxCodeSizeAmsterdam` (32768) / `MaxInitCodeSizeAmsterdam` (65536) and two helpers in + `core/vm/common.go`: `CheckMaxCodeSize(rules, size)` (`IsAmsterdam` → 32768, else `IsEIP158` → + 24576) and `CheckMaxInitCodeSize(rules, size)` (`IsAmsterdam` → 65536, else `IsShanghai` → + 49152). Helpers taken byte-identical. The initcode call sites (`core/state_transition.go`, + `core/txpool/validation.go`, `core/vm/gas_table.go` ×2) auto-merged and are behavior-preserving + pre-Amsterdam; `gasCreate*Eip3860` only runs in Shanghai+ jump tables. + **The code-size call site needed hand-resolution: Bor already raises the same cap under its own + fork.** `MaxCodeSizePostAhmedabad = 32768` is gated on `Bor.IsAhmedabad(blockNumber)` (mainnet + `62278656`, Amoy `11865856` — live today), which is a `BorConfig` block gate rather than a + `params.Rules` field, so upstream's rules-only helper cannot express it; and Bor's + `initNewContract` **assigns `err` and continues** (still charging deployment gas and calling + `SetCode`) where upstream returns immediately. Resolved as: Ahmedabad branch first (Bor's inline + check, unchanged), otherwise upstream's helper with its dormant Amsterdam branch. Both caps are + 32768 so they agree on code size; the initcode limits would differ (49152 → 65536) if Amsterdam + were ever enabled — recorded in fork-register. +- **trienode history alongside existing data (#33934, `7d13acd03`)** — freezer/pathdb changes + adopted, including the `Freezer.frozen` → `Freezer.head` rename, the early-return `doSync` + (Bor's `trackError` variant differed only in wsl blank-line style and error ordering), and the + tail-over-head truncation reset + its new test case. +- **history pruning cutoff in GetFilterLogs (#33823, `189f9d0b1`)** — added the + `HistoryPruningCutoff` / `PrunedHistoryError` check to `GetFilterLogs`, ordered before Bor's + `checkBlockRangeLimit`, matching how `GetLogs` already sequences the two. +- **accessList StorageKeys never null (#33976, `f6068e3fb`)** — adopted upstream's + `slices.SortedFunc` + nil-guard. Bor's unsorted variant dates from the original #22550/#23225 + geth commits plus a Bor wsl-lint pass, not a deliberate divergence, so this is convergence. + `eth_createAccessList` output is now deterministically ordered. +- **fetchpayload utility (#33919, `59512b184`)** — adopted the `fromExtWitness` → + `FromExtWitness` export (keeping Bor's `w.context = ext.Context` line) plus the new + `cmd/fetchpayload`. It is a plain `ethclient`/`rpc` client with no Engine-API dependency and + feeds Bor's existing `cmd/keeper`. +- **Prague pruning points (#33657, `3c20e08cb`)** — adopted in `core/blockchain.go` + (`initializeHistoryPruning` now consults `history.MergePrunePoints` and `PraguePrunePoints`). +- **karalabe/hid bump (#34008)** — FreeBSD ports build fix. Plus #34006's `-signify` flag-name fix + in `build/ci.go`. + +Declined / kept-Bor: + +- **codedb + simplify cachingDB (#33816, `91cec92bf`)** — **whole feature deferred.** A 20-file, + 526-insertion rewrite of the state database layer: new `core/state/database_code.go` (`CodeDB`) + and `core/state/reader_stater.go` (`ReaderStater`), `BlockChain.statedb *state.CachingDB` + replaced by `codedb *state.CodeDB` with the `state.Database` constructed per call + (`state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)`), and — the blocker — the + `ContractCodeReader` interface changes shape (`Code`/`CodeSize` drop their `error` return, `Has` + added). That interface is where Bor's pipelined-SRC instrumentation lives + (`ContractCodeReaderStats`, `ContractCodeReaderWithStats`, `ReaderStats`, `GetStats()`, + `ReadersWithCacheStats()`), consumed by `blockProcessingResult.stats`; upstream replaces the + concrete-typed `GetStats()` with a `state.ReaderStater` type assertion. Adapting means rewriting + Bor's cache-stats reader layer and its parallel/BlockSTM `StateDB` interactions — authoring new + Bor code on a state-root-determinism-critical path. Direct successor to the batch-6 state + code-read-metrics deferral. Reverted `core/blockchain_reader.go`, + `core/blockchain_sethead_test.go`, `core/blockchain_test.go`, + `core/state/{database,database_history,iterator,reader,state_object,statedb,statedb_fuzz_test,statedb_test,stateupdate,sync_test}.go`, + `miner/miner_test.go`, `tests/state_test_util.go`, `triedb/hashdb/database.go` to HEAD; `git rm` + of the two new files and of `core/blockchain_stats.go` (DU, already deleted at HEAD). In + `core/blockchain.go` (shared with #33657) only the #33816 hunks were reverted — **including two + that auto-merged silently**: the `statedb *state.CachingDB` field replaced by + `codedb *state.CodeDB`, and the removal of the snapshot re-init + `bc.statedb = state.NewDatabase(bc.triedb, bc.snaps)` in `setupSnapshot`. See needs-wiring.md. +- **telemetry span for ApplyTransactionWithEVM errors (#33955, `32f05d68a`)** — Bor's tx loop has + no telemetry (OTel deferred since v1.17.0). Both hunks dropped; the error-path `spanEnd(&err)` + had auto-merged into the de-telemetried loop and was removed. +- **remove stale-pivot detection in processSnapSyncContent (#33150, `27c4ca9df`)** — upstream + deletes the block as redundant for its beacon-header-driven path. Bor's forked downloader carries + its own variant (`eth/downloader/bor_downloader.go:2048`, with Bor's `newPivotNum`) and Bor's + snap sync is not beacon-driven, so the rationale doesn't transfer. Kept Bor's deletion of + `eth/downloader/downloader.go` (DU); `bor_downloader.go` untouched. The companion + `eth/catalyst/api.go` hunk auto-merged (catalyst unused in Bor). + +Adaptations to Bor divergences (all found by build/vet): + +- `core/rawdb/{freezer,freezer_resettable}.go` — Bor's offset-aware helpers + (`ItemAmountInAncient`, the `freezer.frozen.Add(offset)` in `NewFreezer`, the open-log) still + used the pre-#33934 field name; renamed to `head`, semantics unchanged. +- `core/rawdb/freezer_table_test.go` — #33934's two new call sites use upstream's `newBatch()`; + Bor's signature is `newBatch(offset uint64)`, so they take `0` like every other Bor call site. +- `core/vm/gas_table_test.go` — restored the `chainConfig := params.AllEthashProtocolChanges` + declaration dropped during conflict resolution while the rest of #33832's hunk auto-merged. +- `eth/tracers/logger/access_list_tracer.go` — added the `slices` import for the adopted sort. + +Verification (per-batch tier): `go build ./...` rc=0; `go vet ./...` clean bar the two pre-existing +`//nolint` copylocks; `go mod tidy` clean (go.mod diff vs batch 16 = karalabe/hid bump only); +`gofmt -l` clean on every changed file except `build/ci.go`, whose misalignment is pre-existing +(HEAD's copy fails the same check; the merge only renamed the `-signify` flag). `go test` pass: +`core/` (184s), `miner/...` (196s), `eth/` (48s), `core/rawdb/...` (62s), `core/vm/...`, +`core/state/...`, `core/stateless/...`, `core/txpool/...`, `eth/filters/...`, `eth/tracers/...`, +`triedb/...` (incl. `pathdb` 57s). Fork guards green (`TestReinforceMultiClientPreCompilesTest`, +`TestBorHardforkPrecompileContinuity*`, `TestV2ForkParity`, `TestCreateGas`). No leftover conflict +markers. + +## v1.17.2 batch 3/4 (`e23b0cbc2`, plan row 18) — merged `1abb57b8f` + +20 first-parent commits, 32 conflicted files (30 content + 2 delete/modify), 30 files in the merge +(+1177 / −259). Decline-heavy: three whole-feature declines cover 27 of the 32 conflicts, and +nothing consensus-affecting was adopted. + +Adopted: + +- **txLookupLock leak fix (#34039, `b6115e9a3`)** — `defer bc.txLookupLock.Unlock()` in `reorg()` + so the early error returns can't leave the mutex held. +- **single storage-trie traversal (#34051, `a3083ff5d`)** — took upstream's `cmd/geth/snapshot.go` + rewrite (`traverseStorage` helper + `--account` flag) wholesale; Bor's only divergences there + were the 4-arg `MakeChainDatabase(ctx, stack, X, false)` (`disableFreeze`, 8 call sites) and + `_, _ =` lint discards on the hasher, both re-applied. +- **binary-trie IntermediateRoot bypass (#34022, `77779d109`)** — adopted the + `&& !s.db.TrieDB().IsVerkle()` prefetcher condition, kept Bor's `skipTimers` guard. Verkle is + dormant on Bor so the branch is never taken. +- **alloc-free flatReader hashing (#34025, `4b915af2c`)** — `addr[:]` / `key[:]` instead of + `.Bytes()`, applied inside Bor's `addrCache` (pipelined-SRC) rather than replacing it. +- **history index initer (#33640, `9b2ce121d`)** — `triedb/pathdb` `NoHistoryIndexDelay` combined + with Bor's `MaxDiffLayers`. +- **rangeLogs invalid-range error (#33763, `3341d8ace`)** — `filter.go` fix auto-merged + (`firstBlock > lastBlock` now returns `errInvalidBlockRange` instead of `nil, nil`); adopted the + matching test expectation while keeping Bor's 4-arg `NewRangeFilter` (Bor carries the range limit + on `api.sys.cfg`, not per-filter). + +Declined / kept-Bor: + +- **miner OpenTelemetry spans (#33773, `98b13f342`)** — **whole feature deferred**, the batch's + largest cluster (23 files, 17 conflicting). Threads `ctx context.Context` through + `consensus.Engine`, the txpool `SubPool` interface, `miner.{BuildPayload,generateWork}`, + `eth/handler`, `eth/sync`, `eth/api_backend` and the whole catalyst surface, purely to attach OTel + spans to block building. Identical class to the batch-12 #33521 decline: Bor's OTel stack was + deferred in v1.17.0 (#33452/#33484) so there is nothing for the spans to attach to, and the + interface changes collide with Bor's VEBLOP miner restructure (`getWorkCh`/async work path) and + its 4-return `FinalizeAndAssemble`. Reverted all 23 files to HEAD; `git rm` for + `eth/catalyst/witness.go` and `miner/payload_building_test.go` (DU, already deleted at HEAD). +- **call-variant gas measurement rework (#33648, `fd859638b`)** — **whole feature deferred.** Splits + each call variant's inner gas calculation into stateless (memory expansion + value transfer + + EIP-2929) and stateful (`Empty`/`Exist` probe, EIP-7702 delegation resolution) halves, with an + early `if contract.Gas < intrinsic { return ErrOutOfGas }` between them so a call that cannot pay + never touches state — preparation for EIP-7928 block access lists. The change is gas-equivalent + (same components, same totals; both orderings end in error-plus-all-gas-consumed when the caller + is short) but it **reorders state reads** and is **not fork-gated**, so on Bor it would shift + witness contents and the BlockSTM MVHashMap read set in out-of-gas cases. Deterministic, hence not + a split risk, but it is an unconditional change to the consensus-critical EVM gas path whose only + beneficiary is dormant. Decisive factor: it lands in `core/vm/{gas_table,operations_acl}.go`, the + exact pair where Bor has already deferred #33281 (write-protection relocated into the gas + handlers), #33637 (per-opcode read-only checks) and #33450 (selfdestruct cold-access early-return), + all blocked behind the #32919 selfdestruct rework — Bor's `gasCall` accordingly has no `readOnly` + check and its `gasCallEIP7702` is a plain alias where upstream now has a BAL-motivated wrapper. + Reverted `core/vm/{gas,gas_table,operations_acl}.go` to HEAD. See needs-wiring.md. +- **history pruning configuration refactor (#34036, `6ae3f9fa5`)** — **whole feature deferred.** + Introduces `history.HistoryPolicy` (user intent) beside the persisted prune point, replaces + `BlockChainConfig.ChainHistoryMode` with `HistoryPolicy`, and rewrites `initializeHistoryPruning`; + `eth/backend.go` builds it via `history.NewPolicy(config.HistoryMode, genesisHash)`. Bor's + `eth/backend.go` **never calls `core.LoadChainConfig`** — it uses `config.Genesis.Config` and a + Bor-specific `CreateConsensusEngine(config.Genesis.Config, config, chainDb, blockChainAPI, vmCfg)` + — so no `genesisHash` is in scope. Adopting means new Bor plumbing for an operational feature Bor + doesn't use: no Polygon genesis hash appears in `MergePrunePoints`/`PraguePrunePoints`, so Bor + nodes are always `KeepAll`. Reverted `cmd/geth/chaincmd.go`, `cmd/workload/testsuite.go`, + `core/blockchain_test.go`, `core/history/historymode.go`, `eth/backend.go`; `git rm` of the new + `core/history/historymode_test.go`. +- **stateless code-database initialization fix (#34011, `a7d09cc14`)** — one-line fix binding + `state.NewCodeDB(memdb)` in `core/stateless.go`; `CodeDB` arrives with #33816, deferred in batch + 17. Kept Bor's form (which also passes `diskdb` to `MakeHashDB`, its own divergence). +- **#33150 / #33955 companions** — no action needed this batch. + +Merge artifact fixed: + +- `core/blockchain.go` — upstream's #34036 rewrite of `initializeHistoryPruning` had auto-merged in + fragments, so resolving only the marked conflict hunks left the function syntactically broken + (orphaned `case` arms after the `switch` was replaced). Restored the file wholesale to HEAD and + re-applied only #34039's two-line `txLookupLock` fix on top. + +Verification (per-batch tier): `go build ./...` rc=0 first try; `go vet ./...` clean bar the two +pre-existing `//nolint` copylocks; `go mod tidy` clean (no dependency change); `gofmt -l` clean on +all changed files. **No fork surface touched** — `git diff HEAD -- params/ core/forkid/ +internal/cli/server/chains/ builder/files/` is empty, no new `params.Rules` field and no new +`forkExpectations` entry. `go test` pass: `core/` (197s), `miner/...` (198s), `eth/` (51s), +`consensus/...` (incl. `consensus/bor` 43s), `core/state/...`, `core/stateless/...`, +`core/rawdb/...` (62s), `core/txpool/...`, `internal/ethapi/...`, `eth/filters/...`, `triedb/...` +(incl. `pathdb` 76s). Two failing packages, both **proven pre-existing** by re-running the same +tests on a clean worktree at batch-17 tip `0a83ed542`: `cmd/geth` (`TestConsoleWelcome`, +`TestCustomBackend`, `TestCustomGenesis`, `TestExport`, `TestAttachWelcome` — the documented +VEBLOP/non-Bor-genesis nil-deref class; `TestAttachWelcome` fails identically at 360s with all +three subtests timing out) and `core/vm` (`TestAbortDuringJump`, `TestInterruptDuringExecution` — +the documented interrupt-timing flake). Fork guards green +(`TestReinforceMultiClientPreCompilesTest`, `TestBorHardforkPrecompileContinuity*`, +`TestV2ForkParity`). No leftover conflict markers. + +## v1.17.2 batch 4/4 (`be4dc0c4b`, plan row 19) — merged `682b4c380` — MILESTONE-CLOSING + +17 first-parent commits, 12 conflicted files, 50 files in the merge (+689 / −135). The batch's +weight is EIP-7708, the first Amsterdam EIP whose content had to be reshaped rather than copied: +three separate adaptations were needed because Bor's transfer, selfdestruct, and parallel-state +paths all diverge from upstream's. + +Adopted: + +- **EIP-7708, ETH transfers as logs (#33645, `b87340a85`)** — **wired dormant** behind Bor's + block-based Amsterdam gate. Two new topic constants in `params/protocol_params.go` + (`EthTransferLogEvent`, `EthBurnLogEvent`), `types.EthTransferLog` / `types.EthBurnLog`, + `StateDB.EmitLogsForBurnAccounts` (+ the `vm.StateDB` interface entry and the hooked + forwarder), the `rules.IsAmsterdam` call in `stateTransition.execute`, and the + `Transfer`/`TransferFunc` signature change to carry `*params.Rules`. Three adaptations below. +- **witness stats relocation (#34106, `c3467dd8b`)** — stats move from `StateDB`/`BlockChain` + locals into `Witness` itself: `NewWitness` gains `enableStats bool`, `AddState` gains + `owner common.Hash`, `StartPrefetcher` drops its stats parameter, `Witness.ReportMetrics` + replaces the caller-side reporting. No consensus effect; swept across ~25 Bor call sites. +- **eth_getProof key cap (#34617, `95705e8b7`)**, **eth_simulateV1 block/call caps (#34616, + `ceabc3930`)**, **`vm.ErrMaxInitCodeSizeExceeded` RPC error remap (#34067, `a61e5ccb1`)**, + **freezer `dir.Sync()` Windows split (#34115, `e585ad3b4`)**, **`types.Sender` in legacypool + (#34059, `d1369b69f`)**, **womir keeper target (#34079, `bd3c8431d`)**, **discv5 bootstrap DNS + resolution (#34101, `a2496852e`)** — all auto-merged, verified present. +- **discv5 PingMultiIP session key (#34031, `e951bcbff`)** — decode against `tc.remoteAddr` + rather than the packet source address; Bor's only divergence in the hunk was wsl blank lines. + +### EIP-7708 adaptation 1 — log ordering across Bor's two transfer paths + +Upstream appends the transfer log directly after the balance change in `Transfer`. Bor has two +transfer functions, and the Bor one has an early-return fast path: + +- `Transfer` — additionally emits the 0x1010 `LogTransfer`, and returns early when + `db.RecordTransfer` reports that V2 BlockSTM captured the transfer for settlement. +- `EthereumTransfer` — the no-log variant used when the chain carries no `Bor` config + (execution-spec-tests). It now carries the EIP-7708 emission too, making it byte-equivalent to + upstream's `Transfer`. + +The emission was factored into `emitEthTransferLog` and called at the same point relative to the +balance change on all three paths. This is load-bearing, not cosmetic: on the V2 path Bor's 0x1010 +log is generated later, at settlement, so placing the EIP-7708 log after `AddTransferLog` on the +serial path would order the pair one way under V1 and the other way under V2 — a receipt-root +divergence that would only surface once Amsterdam is enabled. + +Recorded for the enablement decision: with Amsterdam active, a plain value transfer on Bor emits +**both** the EIP-7708 system log and the existing 0x1010 `LogTransfer`. Bloom and receipt-size +change, not a correctness problem, but a deliberate product call. See fork-register.md. + +### EIP-7708 adaptation 2 — selfdestruct without #32919 + +Upstream's `opSelfdestruct6780` hunk reads `newContract` from `StateDB.IsNewContract`, which +arrives with the #32919 selfdestruct rework Bor declined. Instead of declining the branch, the +signal was recovered from Bor's own API: `SelfDestruct6780` already returns +`(balance, wasNewContract)` and the opcode handler was discarding both. Capturing the second +return reproduces upstream's semantics exactly, verified across all four cases against Bor's +shape (which always does `SubBalance(this)` + `AddBalance(beneficiary)` before the call): +`this != beneficiary` → transfer log regardless of newness; `this == beneficiary` → burn log only +when the contract was created in the same tx. + +Declining would **not** have been covered by the adopted `EmitLogsForBurnAccounts` safety net: +Bor's `SelfDestruct6780` → `SelfDestruct` zeroes the balance in place, so the tx-boundary sweep +(which requires `!obj.Balance().IsZero()`) skips the account and the burn log is lost silently. + +### EIP-7708 adaptation 3 — `ParallelStateDB.EmitLogsForBurnAccounts` + +The new `vm.StateDB` method broke Bor's `ParallelStateDB`. Implemented against the parallel +executor's `destructed` map, address-sorted to match the serial executor's ordering (V1 sorts its +journal dirties for the same reason). Dead while Amsterdam is dormant; exists for V1/V2 parity. + +### EIP-7708 test adaptation + +Upstream's new `core/eth_transfer_logs_test.go` activates the fork with +`config.AmsterdamTime = new(uint64)` and pokes `BlobScheduleConfig.Amsterdam`; Bor is block-based +and carries no Amsterdam blob schedule, so the setup is `config.AmsterdamBlock = new(big.Int)`. +`MergedTestChainConfig` carries a `Bor` stanza, so the scenario runs the Bor transfer path and the +receipt interleaves 0x1010 logs. Rather than hardcode Bor's gas-dependent balance-snapshot +payloads, the assertion filters fee-address logs out and pins the interleaving through each +expected log's `Index`. Index 6 (the SELFDESTRUCT transfer) having no 0x1010 sibling is the direct +observable consequence of adaptation 2. + +### #34106 Bor-specific fallout + +- `core/state/statedb.go` kept its `witStart` / `s.WitnessCollection` timer around account-trie + witness collection. +- Bor's V2-only `CollectStateWitness` / `trieReader.CollectStateWitness` callback grew the owner + hash: main trie passes the zero hash, each sub-trie passes `crypto.Keccak256Hash(addr[:])`. + Passing the zero hash for sub-tries would have misfiled every V2 worker-read storage node as an + account-trie access in the stats. +- `core/stateless/witness.go` kept Bor's `sync.RWMutex` while taking upstream's new `stats` field. +- The now-redundant `witnessStats` local in Bor's `nolint:unused` `processBlock` was removed; + `insertChain`'s two `NewWitness` sites pass `bc.cfg.VmConfig.EnableWitnessStats`, preserving + Bor's existing opt-in. + +Declined: + +- **history import batching (#33894, `8f9061f93`)** — **whole commit deferred.** Rewrites + `ImportHistory`'s per-block insert into a batched flush; six hunks conflicted. Bor's copy of the + function is independently diverged — `era.NewIterator(e)` rather than `e.Iterator()`, a `forker`, + and header insertion via `chain.HeaderChain().InsertHeaderChain` before the receipt chain, none + of which upstream does. Throughput-only change on an offline CLI path with no consensus surface; + hand-merging batched flush semantics into Bor's header-inserting loop risks silently producing a + corrupt imported history db. Reverted to HEAD, filed against the era cluster (#32157). +- **OTel SampleRatio IsSet guard (#34062, `745b0a8c0`)** — dependent decline; the whole + `setOpenTelemetry` function is absent from Bor's `cmd/utils/flags.go` (OTel line declined in + v1.17.0, #33452 / #33484). +- **slot number in test payload (#34094, `acdd13971`)** — dependent decline; the only changed line + is inside `BuildTestingPayload`, which Bor deleted with `testing_buildBlockV1` (#33656). +- **v1.17.2 release commit (`be4dc0c4b`)** — `version/version.go` auto-took the bump to + `1.17.2-stable`; reverted to Bor's `Major=1 / Minor=17 / Patch=0 / Meta="unstable"`. + +Verification (per-batch tier): `go build ./...` clean; `go vet ./...` clean bar the two pre-existing +`//nolint` copylocks; `gofmt -l` clean on all changed files; `go mod tidy` no-op. **Fork surface is +`params/protocol_params.go` only** (+7, two event-topic constants) — no new fork gate, no new +`params.Rules` field, no new `forkExpectations` entry, and the diff against `core/forkid/`, +`internal/cli/server/chains/` and `builder/files/` is empty. Both topic constants were verified +against `crypto.Keccak256Hash` rather than trusted from the diff. Fork guards green +(`TestReinforceMultiClientPreCompilesTest`, `TestBorHardforkPrecompileContinuity*`, +`TestV2ForkParity`). `go test` pass: `core/` (181s), `core/state/...`, `core/stateless/...`, +`core/vm/runtime`, `core/vm/program`, `miner/...` (199s), `params/...`, `consensus/bor/...`, +`core/rawdb/...` (56s), `core/txpool/...`, `core/types/...`, `core/tracing`, `eth/` (51s), +`eth/fetcher/...` (140s), `eth/protocols/...`, `internal/ethapi/...`, `p2p/discover/...` (31s), +`trie/...`, `cmd/devp2p`. Two failing packages, both **proven pre-existing** on a detached worktree +at batch-18 tip `1abb57b8f`: `core/vm` (`TestAbortDuringJump`, `TestInterruptDuringExecution` — the +documented interrupt-timing flake; baseline fails 5 of 6 runs with byte-identical assertions, the +branch 2 of 3, no rate change) and `cmd/devp2p/internal/ethtest` (`BorConfig.CalculatePeriod` +nil-deref panic from `miner.newWorkLoop` — identical panic and stack at baseline, the documented +VEBLOP/non-Bor-genesis class). No leftover conflict markers. + +## v1.17.2 milestone triage (upstream PRs `16783c167..be4dc0c4b`, 77 first-parent commits) + +The wiring pass over the whole milestone, independent of which commits conflicted: an upstream PR +can merge completely clean and still leave Bor un-wired. Classification per §6.1. + +**One finding needing a decision, one confirmation, the rest inert.** + +| Class | Count | PRs | +| --- | --- | --- | +| **needs wiring** | 1 | #33836 / #33975 default cache bump | +| **consensus-relevant** | 5 | #33593 (EIP-7778), #33869 (EIP-8024 update), #33947 (Amsterdam jump table), #33832 (EIP-7954), #33645 (EIP-7708) | +| **operator-visible, no wiring** | 7 | #34617, #34616, #32789/#33952, #33763, #33976, #34005, #34101 | +| **deferred** (rows in needs-wiring.md) | 12 | #33931, #33816, #33773, #33648, #34036, #34011, #33894, #34062, #34094, #33950, #33955, #33150 | +| **inert** | 52 | refactors, bugfixes, tests, build, dependency bumps, dormant-feature work | + +### needs wiring — default cache bump (#33836, #33975) + +The one case in this milestone where a clean merge left Bor's behavior unchanged when it looks +like it changed. Upstream raised the default total cache 1024 MB → 4096 MB, which shows up in this +milestone's diff as `ethconfig.Defaults` moving to `DatabaseCache 2048 / TrieCleanCache 614 / +TrieDirtyCache 1024 / SnapshotCache 409`. Bor's `bor server` path never reads those: `internal/cli/ +server/config.go` carries its own `Cache: 1024` and a 50/15/25/10 percentage split whose `calcPerc` +output overwrites all four fields, and that split against 1024 MB reproduces geth's **old** defaults +(512 / 153 / 256 / 102) exactly. So the bump reaches `cmd/geth` and tests but not Bor's production +startup. Deliberately not taken — quadrupling the default memory footprint is a PoS/devops call. +Row filed in `needs-wiring.md` with the one-line change if the team wants parity. + +### consensus-relevant — register review + +Reviewed `fork-register.md` for completeness across the milestone. All five entries are +**enable-by-block, dormant**, gated on `IsAmsterdam`, which is `nil` on every shipped Bor preset: +EIP-7778 (batch 16), the EIP-8024 branchless/EXCHANGE update and the Amsterdam jump-table dispatch +(batch 16), EIP-7954 (batch 17), EIP-7708 (batch 19). No fork block, `params.Rules` field, forkid +input, chain preset, or genesis file changed anywhere in the milestone. Two entries carry +enable-time consequences beyond "set the block" and are recorded as such in the register: EIP-7954 +overlaps Bor's live Ahmedabad code-size cap (and would move initcode 49152 → 65536), and EIP-7708 +duplicates Bor's 0x1010 `LogTransfer` while depending on a workaround for the declined #32919. + +### operator-visible, no wiring + +Behavior changes an operator could notice but which need no Bor-side plumbing: hardcoded RPC caps +on `eth_getProof` keys (#34617) and `eth_simulateV1` blocks/calls (#34616); the new `MaxUsedGas` +field in the `eth_simulateV1` response (#32789, gas-cap fix #33952); `eth_getLogs` now erroring on +an inverted block range instead of returning `nil, nil` (#33763); `eth_createAccessList` returning +`[]` rather than `null` for empty `StorageKeys` (#33976); hex-encoded `slotNumber` in +`RPCMarshalHeader` (#34005, dormant — Bor's header field is nil pre-Amsterdam); and DNS hostname +resolution for bootstrap nodes (#34101). + +### inert + +The remaining 52 are internal refactors, bugfixes, test fixes, build changes and dependency bumps +with no Bor-facing surface. Two sub-groups worth naming rather than listing: the binary-trie work +(#34056, #34032, #33989, #33961, #33951, #34021, #34022) is real but sits behind Verkle/bintrie, +dormant on Bor; and the new upstream tooling that merged in unused (`cmd/fetchpayload` #33919, the +`womir` keeper target #34079) builds clean and needs no Bor wiring — Bor ships its CLI from +`internal/cli`, not `cmd/geth`. + +## v1.17.2 milestone full-gate (invariant 8) + +Run at milestone tip `682b4c380`. + +| Gate | Result | +| --- | --- | +| `go build ./...` | clean | +| `go vet ./...` | clean bar the two pre-existing `//nolint` copylocks | +| `make lint` (golangci-lint v2.11.4, repo `.golangci.yml`) | **0 issues** | +| `gofmt -l` / `go mod tidy` | clean / no-op | +| `go test ./...` | **148 packages pass**, 4 fail — all pre-existing (below) | +| `make test-integration` | **`tests/bor` ok, 628 s, 77.6% coverage**; `tests` ok but see caveat | +| govulncheck | 3 called vulns, unchanged from before the milestone | +| kurtosis devnet | **8/8 checks pass** + EIP-7708 dormancy proof (below) | + +### Pre-existing test failures (no new names) + +`cmd/geth` (`TestAttachWelcome` 360 s, `TestConsoleWelcome`, `TestCustomBackend`, +`TestCustomGenesis`, `TestExport`) and `cmd/devp2p/internal/ethtest` — the VEBLOP/non-Bor-genesis +nil-deref class; `cmd/evm` (`TestT8n`, `TestEVMTracing`, `TestEvmRun`, `TestEvmRunRegEx`) — t8n +golden drift; `core/vm` (`TestAbortDuringJump`) — the interrupt-timing flake. `ethtest` and +`core/vm` were re-baselined on a detached worktree at `1abb57b8f` during batch 19 and fail +identically there. + +### Caveat — `make test-integration`'s `./tests` package is a no-op here + +It reports `ok` in 1.2 s at 0% coverage because `tests/testdata` (the ethereum consensus fixtures) +is absent, and `.gitmodules` defines only `tests/evm-benchmarks`, so CI's +`git submodule update --init --recursive` doesn't fetch them either — `TestState`, `TestBlockchain`, +`TestTransaction`, `TestRLP` and `TestDifficulty` all skip rather than fail. Pre-existing repo +property, not caused by this milestone, but worth knowing: the integration signal comes from +`tests/bor`, not from the upstream consensus suite. + +### govulncheck — unchanged + +Three called vulnerabilities, identical to before the milestone, all existing team follow-ups: +`GO-2026-5970` (`golang.org/x/text@v0.37.0`, fixed in 0.39.0), `GO-2026-5932` +(`golang.org/x/crypto@v0.51.0`, no fix available), `GO-2026-5856` (`crypto/tls@go1.26.4`, fixed in +Go 1.26.5). No new entries from this milestone's dependency bumps. + +### Kurtosis devnet — EIP-7708 dormancy proven on a live chain + +`small` preset (1 validator + 1 RPC + bridge/tx spammers + observability) on `bor:v1172-sync`, +built from this tip. Block production ~1 s with validator and RPC in lockstep (168 → 236 over +80 s), 45–100 txs/block, 29 `StateSynced` logs, span 3 active, checkpoint 6 submitted, zero +`ERROR`/`panic`/`FATAL` in either client's logs. + +The check that matters for this milestone: over the last 50 blocks, + +| Log source | Count | +| --- | --- | +| `0xfffffffffffffffffffffffffffffffffffffffe` (EIP-7708 `SystemAddress`) | **0** | +| `0x0000000000000000000000000000000000001010` (Bor `LogTransfer`) | **2598** | + +The 2598 is what makes the 0 meaningful — thousands of value transfers went through +`core.Transfer` in that window and none emitted a system log. A leaked gate, or an emission placed +outside the `rules.IsAmsterdam` check on any of the three transfer paths (serial, V2 BlockSTM fast +path, `EthereumTransfer`), would have produced a non-zero count. Report: +`runs/pos-spawn-devnet/2026-07-27T08-07-02Z-claude-pos-v1172-sync/summary.md`. diff --git a/docs/upstream-merges/v1.17.4/needs-wiring.md b/docs/upstream-merges/v1.17.4/needs-wiring.md index ea0c5c6acb..360da9e427 100644 --- a/docs/upstream-merges/v1.17.4/needs-wiring.md +++ b/docs/upstream-merges/v1.17.4/needs-wiring.md @@ -49,6 +49,20 @@ a future adoption would require. | On-chain tx check in fetcher (#33607) | batch 14 (v1.17.1 1/2, `9ecb6c4ae`) | deferred | Threads a new `chain *core.BlockChain` (nil-able) through `NewTxFetcher`/`NewTxFetcherForTests`, adds a `txOnChainCache` + `chain` field to `TxFetcher`, a `txAnnounceOnchainMeter`, and an on-chain existence check in `Notify` (skip announcing txs already on chain). Builds directly on #33378's `validateMeta func(common.Hash, byte) error` + `txMetadataWithSeq` fetcher internals, which Bor **declined** (batch-6 row) — Bor keeps `hasTx func(common.Hash) bool` + `txMetadata`. The upstream `Notify` hunk even references #33378's `err` var. #33607 was the sole batch-14 commit touching all 5 of its files, so restored `eth/fetcher/{tx_fetcher,tx_fetcher_test,metrics}.go`, `eth/handler.go`, `tests/fuzzers/txfetcher/txfetcher_fuzzer.go` wholesale to Bor HEAD. | Adopt together with the batch-6 #33378 row: once Bor's fetcher migrates to `validateMeta`/`txMetadataWithSeq`, thread the nil-able `chain` param, add `txOnChainCache` population (mark-canonical / reorg) + the `Notify` on-chain skip. Perf/DoS-positive (avoids fetching already-chained txs). | upstream boundary `9ecb6c4ae`; commit `59ad40e56` (#33607); depends on the batch-6 #33378 row | | testing_buildBlockV1 Engine API (#33656) | batch 14 (v1.17.1 1/2, `9ecb6c4ae`) | deferred | Adds a test-only `testing_buildBlockV1` catalyst Engine-API method: new `generateParams.{forceOverrides,overrideExtraData,overrideTxs}` fields, an override branch in `generateWork` (commit override txs instead of `fillTransactions`), override plumbing in `miner/payload_building.go` + `internal/ethapi/override`, `beacon/engine/types.go` + `eth/catalyst/{api,simulated_beacon}.go` wiring, and new `eth/catalyst/api_testing{,_test}.go`. Catalyst is inherited-but-unused in Bor's production consensus, and the worker.go changes collide head-on with Bor's VEBLOP `getWork`/`getWorkReq` async restructure (Bor rewrote the whole `generateWork` region — "Sanitize recommit interval" vs upstream's fillTransactions). #33656 was the sole batch-14 commit touching all its files, so reverted the 8 modified files to Bor HEAD and `git rm`'d the 2 new files. | Adopt only if the catalyst testing API is ever needed on Bor: adapt the override fields into Bor's restructured `generateParams`/`getWorkReq`, thread the override-commit branch through Bor's async work path, and re-add the catalyst wiring. Test-only; low priority. | upstream boundary `9ecb6c4ae`; commit `e40aa46e8` (#33656) | | Drop eth/68 protocol version (#33511) | batch 15 (v1.17.1 2/2, `16783c167`) | deferred | 695-line deletion across 16 files removing eth/68 support: unifies `StatusPacket68`/`StatusPacket69`→`StatusPacket`, drops `NewBlockHashesPacket`/eth-68 receipt handling, reworks handshake/handlers/receipt. Entangled with Bor divergences: forked downloader (`bor_downloader.go`, `bor_fetchers_concurrent_bodies.go`), wit protocol, the receipt interface's Bor-only `ExcludeStateSyncReceipt()`, Bor's NewBlock push metrics (`newBlockPushIntervalTimer`/`lastNewBlockPushUnix`), and Bor's PoA block-announcement propagation (Bor still broadcasts blocks via the eth protocol, unlike post-Merge geth). Same class as the deferred #33835. Reverted the full 16-file footprint to HEAD (`eth/protocols/eth/{handler,handler_test,handlers,handshake,handshake_test,peer,protocol,protocol_test,receipt,receipt_test}.go`, `eth/handler_eth_test.go`, `eth/downloader/skeleton_test.go`, `eth/sync_test.go`, `cmd/devp2p/internal/ethtest/{conn,suite}.go`); `git rm` of DU `eth/downloader/downloader_test.go`. Two piggybacked commits reverted with it: **#2a4527240** (handshake timeout-metrics classification, 12 lines in handshake.go) and **#cee751a1e** (flaky `TestSnapSyncDisabling68` fix, sync_test.go) — both re-adoptable independently. | Adopt the eth/68 drop onto Bor's protocol layer alongside #33835: reconcile the unified `StatusPacket` + receipt interface with Bor's `ExcludeStateSyncReceipt()`, preserve NewBlock push metrics + PoA announcement path, and port into Bor's forked downloader/wit. Re-apply #2a4527240 (metrics fix) and #cee751a1e (test) at the same time. | upstream boundary `16783c167`; commit `723aae2b4` (#33511); relates to the batch-12 #33835 row | +| Prevent state flushing in RPC / `ExecuteConfig` (#33931) | batch 16 (v1.17.2 1/4, `00540f946`) | deferred | Introduces `core.ExecuteConfig{WriteState,WriteHead,EnableTracer,MakeWitness,StatelessSelfValidation,EnableWitnessStats}` threaded through `BlockChain.ProcessBlock`, moves `StatelessSelfValidation`/`EnableWitnessStats` out of `vm.Config` into `BlockChainConfig`, gates the block-write + tracer blocks on the config, and rewrites `debug_executionWitness` (now `BlockNumberOrHash`, absorbing `ExecutionWitnessByHash`) to execute with `WriteState: false` so an RPC call no longer persists state. Bor has already forked every target: its live `ProcessBlock` is the tuple-returning `(block, parent, witness, followupInterrupt)` form at `core/blockchain.go:857` (upstream's survives only as dead `processBlock`, `// nolint : unused`), `insertChain` is restructured around Bor's direct-timer metrics, and `eth/api_debug.go` has its own `ExecutionWitness`/`ExecutionWitnessByHash` pair built on the Bor signature — which returns a statedb without persisting, so **Bor does not exhibit the bug being fixed**. Adopting would mean authoring new Bor plumbing rather than merging upstream code. Reverted `core/vm/interpreter.go`, `eth/api_debug.go`, `eth/backend.go`, `internal/web3ext/web3ext.go`, `tests/block_test_util.go` wholesale, and the #33931 hunks only in `cmd/utils/flags.go` + `core/blockchain.go` (shared with #33836 / #33708). | Adopt if Bor ever converges its block-execution entry points on upstream's: add `ExecuteConfig` to Bor's `ProcessBlock`, move the two witness flags from `vm.Config` to `BlockChainConfig` (updating `bc.cfg.VmConfig.StatelessSelfValidation` / `EnableWitnessStats` readers in `insertChain`/`processBlock`), gate Bor's write path on `WriteState`, and merge Bor's two `ExecutionWitness*` RPCs into the single `BlockNumberOrHash` form. Verify Bor's witness RPC really is non-persisting before treating it as a no-op. | upstream boundary `00540f946`; commit `6d99759f0` (#33931) | +| Engine-API miner stress harness (`miner/stress`, added by #33593) | batch 16 (v1.17.2 1/4, `00540f946`) | deferred | New 210-line `miner/stress/main.go` — a block-builder stress test driven through the Engine API (`eth/catalyst` simulated beacon) that the EIP-7778 PR added alongside the gas-accounting change. Needs three surfaces Bor doesn't have: `params.ChainConfig.AmsterdamTime` (Bor's Amsterdam gate is block-based, `AmsterdamBlock`), `BlobScheduleConfig.Amsterdam`, and `ethconfig.Config.SlowBlockThreshold` (slow-block stats deferred in v1.17.0). Catalyst is inherited-but-unused in Bor's production consensus. Removed the file. | Adopt only if a Bor-side block-builder stress harness is wanted: rewrite the fork setup against Bor's block-based gates (or drive it through Bor's PoA miner instead of the Engine API), and either adopt slow-block stats or drop the `SlowBlockThreshold` knob. Dev tool; low priority. | upstream boundary `00540f946`; commit `6d0dd0886` (#33593) | +| codedb + simplify cachingDB (#33816) | batch 17 (v1.17.2 2/4, `77e7e5ad1`) | deferred | 20-file, 526-insertion rewrite of the state database layer: new `core/state/database_code.go` (`CodeDB`, a standalone contract-code store with its own cache) and `core/state/reader_stater.go` (`ReaderStater` interface), `BlockChain.statedb *state.CachingDB` replaced by `codedb *state.CodeDB` with the `state.Database` built per call (`state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)`), and the `ContractCodeReader` interface reshaped — `Code`/`CodeSize` drop their `error` return and `Has(addr, codeHash) bool` is added. Blocker: that interface is the anchor of Bor's pipelined-SRC prefetch/cache-stats instrumentation (`ContractCodeReaderStats`, `ContractCodeReaderWithStats`, `ReaderStats`, `GetStats()`, `ReadersWithCacheStats()`), whose output feeds `blockProcessingResult.stats` and Bor's per-block Datadog metrics; upstream drops the concrete-typed `GetStats()` in favour of a `state.ReaderStater` type assertion. Adapting means rewriting Bor's cache-stats reader layer plus its parallel/BlockSTM `StateDB` interactions — authoring new Bor code on a state-root-determinism-critical path, not merging upstream's. Direct successor to the batch-6 state code-read-metrics row (same surface). Reverted the full footprint to HEAD; `git rm` of `core/state/{database_code,reader_stater}.go` and `core/blockchain_stats.go` (DU); in `core/blockchain.go` (shared with #33657) reverted only the #33816 hunks, **including two that auto-merged silently** — the `statedb *state.CachingDB` field being replaced by `codedb *state.CodeDB`, and the removal of the snapshot re-init in `setupSnapshot`. | Adopt together with the batch-6 state code-read-metrics row: (1) decide whether Bor keeps its own cache-stats reader layer or migrates to `state.ReaderStater`; (2) port `CodeDB` + the per-call `state.Database` construction while preserving Bor's `CommitWithUpdate`/`stateUpdate` and snapshot re-init; (3) drop the `error` returns from Bor's `Code`/`CodeSize` implementers (incl. the parallel/BlockSTM readers and test helpers) and add `Has`; (4) re-verify prefetch/process cache-stat metrics still populate. Perf/structure cleanup, not consensus — but touches state-root-determinism code, so it needs its own review + differential testing. | upstream boundary `77e7e5ad1`; commit `91cec92bf` (#33816); relates to the batch-6 state code-read-metrics row | +| Stale-pivot detection removal in processSnapSyncContent (#33150) | batch 17 (v1.17.2 2/4, `77e7e5ad1`) | wontfix | Upstream deletes the "pivot became stale, moving" block from `eth/downloader/downloader.go`, arguing it is redundant because a stale pivot is already detected by `fetchHeaders` on the beacon-header-driven path. Bor deleted upstream's `downloader.go` long ago and runs a forked downloader; the equivalent block still lives at `eth/downloader/bor_downloader.go:2048` with Bor-specific handling (`newPivotNum`). Bor's snap sync is not beacon-driven, so upstream's redundancy argument does not transfer. Kept Bor's file deletion (DU) and left the forked downloader untouched; the companion `eth/catalyst/api.go` hunk auto-merged (catalyst unused in Bor). | n/a for a merge batch — if Bor ever wants the simplification it needs its own analysis of whether `fetchHeaders` covers stale pivots on the Bor sync path, plus snap-sync soak testing. | upstream boundary `77e7e5ad1`; commit `27c4ca9df` (#33150); `eth/downloader/bor_downloader.go` | +| Miner OpenTelemetry spans / ctx-threading (#33773) | batch 18 (v1.17.2 3/4, `e23b0cbc2`) | deferred | 23-file change threading `ctx context.Context` through `consensus.Engine` (`FinalizeAndAssemble`, `Finalize`), the txpool `SubPool` interface (`Pending`), `miner.{BuildPayload,generateWork,commitWork}`, `eth/handler`, `eth/sync`, `eth/api_backend`, `internal/ethapi/simulate.go` and the whole `eth/catalyst` surface (incl. simulated-beacon server spans for `geth --dev`), purely so OpenTelemetry spans can be attached to the block-building path. Third instance of the same class (batch-9 #33452, batch-11 #33484, batch-12 #33521): Bor's OTel stack is deferred, so there is nothing for the spans to attach to, and the signature changes collide with Bor's VEBLOP miner restructure (`getWorkCh`/async work path, `newWorkReq`) and Bor's 4-return `FinalizeAndAssemble(chainHeadReader, header, state, body, receipts) (block, receipts, _, err)`. Reverted all 23 files to HEAD; `git rm` of `eth/catalyst/witness.go` + `miner/payload_building_test.go` (DU, already deleted at HEAD). | Adopt with the rest of the OTel line (batch-9/11/12 rows): once `internal/telemetry` is back and a ctx-threading strategy compatible with Bor's `interruptCtx`/`author` params is chosen, add the leading `ctx` to Bor's consensus/txpool/miner signatures and attach the block-building spans. Observability only. | upstream boundary `e23b0cbc2`; commit `98b13f342` (#33773); relates to the batch-9 #33452, batch-11 #33484 and batch-12 #33521 rows | +| Call-variant gas measurement rework (#33648) | batch 18 (v1.17.2 3/4, `e23b0cbc2`) | deferred | Splits the inner gas calculation of CALL/CALLCODE/DELEGATECALL/STATICCALL into a stateless component (memory expansion, value transfer, EIP-2929 cold access) and a stateful one (`Empty`/`Exist` account probe, EIP-7702 delegation resolution), inserting an early `if contract.Gas < intrinsic { return ErrOutOfGas }` so a call that cannot pay never reads state — required by EIP-7928 (block access lists), which brings state reads into consensus. Gas-equivalent (identical components and totals; both orderings end in error-plus-all-gas-consumed when the caller is short) but it **reorders state reads** and is **not fork-gated**, so on Bor it shifts witness contents and the BlockSTM MVHashMap read set in out-of-gas cases — deterministic, but an unconditional change to the consensus-critical EVM gas path whose only beneficiary (BAL) is dormant on Bor. It also lands in `core/vm/{gas_table,operations_acl}.go`, the same pair already carrying three deferred upstream changes (#33281, #33637, #33450, blocked behind #32919), where Bor's `gasCall` has no `readOnly` check and `gasCallEIP7702` is a plain alias rather than upstream's BAL-motivated wrapper. Reverted `core/vm/{gas,gas_table,operations_acl}.go` to HEAD. | Adopt as part of a single coordinated `core/vm` catch-up together with #33281/#33637/#33450/#32919, ideally when the PoS team decides on EIP-7928/BAL: take upstream's `makeCallVariantGasCost` + `gas*Intrinsic` split and the reworked `makeCallVariantGasCallEIP7702`, then re-verify Bor's BlockSTM read-set determinism and witness output at the out-of-gas boundary. Prerequisite for the M5/M6 BAL batches. | upstream boundary `e23b0cbc2`; commit `fd859638b` (#33648); relates to the batch-9 write-protection cluster rows | +| History pruning configuration refactor (#34036) | batch 18 (v1.17.2 3/4, `e23b0cbc2`) | deferred | Introduces `history.HistoryPolicy` (captures user *intent*) alongside the persisted prune point (actual db tail), replaces `BlockChainConfig.ChainHistoryMode` with `HistoryPolicy`, rewrites `initializeHistoryPruning` around `policy.Target`, and relaxes the semantics (an externally pruned db now warns instead of refusing to start). `eth/backend.go` constructs it via `history.NewPolicy(config.HistoryMode, genesisHash)`. Bor's `eth/backend.go` never calls `core.LoadChainConfig` — it takes `config.Genesis.Config` directly and uses a Bor-specific `CreateConsensusEngine(config.Genesis.Config, config, chainDb, blockChainAPI, vmCfg)` — so no `genesisHash` is in scope, and supplying one is new Bor plumbing for a feature Bor doesn't use (no Polygon genesis hash appears in `MergePrunePoints`/`PraguePrunePoints`, so Bor nodes are always `KeepAll`). Reverted `cmd/geth/chaincmd.go`, `cmd/workload/testsuite.go`, `core/blockchain_test.go`, `core/history/historymode.go`, `eth/backend.go`; `git rm` of the new `core/history/historymode_test.go`; `core/blockchain.go` restored to HEAD (shared with #34039). | Adopt if Bor ever wants history expiry: derive the genesis hash in Bor's `New()` (either add a `core.LoadChainConfig` call or reuse the already-loaded genesis), build the policy, swap `ChainHistoryMode` → `HistoryPolicy` in `BlockChainConfig`, and take upstream's `initializeHistoryPruning`. Operational, not consensus. Supersedes the #33657 Prague-prune-points version adopted in batch 17. | upstream boundary `e23b0cbc2`; commit `6ae3f9fa5` (#34036) | +| Stateless code-database initialization fix (#34011) | batch 18 (v1.17.2 3/4, `e23b0cbc2`) | deferred | One-line fix in `core/stateless.go` binding the statedb's code source to the stateless input: `state.NewDatabase(triedb.NewDatabase(memdb, …), state.NewCodeDB(memdb))` instead of a nil code reader. `state.CodeDB` is introduced by #33816, deferred in batch 17, so the fix cannot be applied on its own. Kept Bor's form (which additionally passes `diskdb` to `witness.MakeHashDB`, a Bor divergence). | Re-apply automatically when the #33816 codedb row is adopted — it is the same one-line change, preserving Bor's `MakeHashDB(diskdb)` argument. | upstream boundary `e23b0cbc2`; commit `a7d09cc14` (#34011); blocked on the batch-17 #33816 row | +| eth/fetcher chain-event unsubscribe nil-guard (#33950) | batch 16 (v1.17.2 1/4, `00540f946`) | coverage-gap | Wraps the `defer sub.Unsubscribe()` of the fetcher's chain-event subscription in `if sub != nil` to de-flake a test. Bor has no such subscription — the `headEventCh`/`SubscribeChainEvent` block arrived with #33378, which Bor declined (batch-6 row) — so there is nothing to guard. Kept Bor's loop. | Re-apply automatically when the batch-6 #33378 row is adopted; the guard is one `if` around the existing defer. | upstream boundary `00540f946`; commit `344ce84a4` (#33950); depends on the batch-6 #33378 row | +| History import batched insertion (#33894) | batch 19 (v1.17.2 4/4, `be4dc0c4b`) | deferred | Rewrites `ImportHistory`'s per-block `InsertReceiptChain` into an `importBatchSize` accumulator with a `flush()` closure, adds `defer e.Close()`, and reworks the checksum loop. Six hunks conflicted because Bor's copy of the function is independently diverged from upstream: it iterates via `era.NewIterator(e)` rather than `e.Iterator()`, carries a `forker`, and inserts headers through `chain.HeaderChain().InsertHeaderChain` before the receipt chain — none of which upstream does. Throughput-only change on an offline CLI path (`geth import-history`) with no consensus surface; hand-merging batched flush semantics into Bor's header-inserting loop risks silently producing a corrupt imported history db for no benefit Bor currently needs. Reverted `cmd/utils/cmd.go` to HEAD. | Adopt as part of a broader era catch-up together with the #32157 EraE row: first reconcile Bor's `ImportHistory` with upstream's current shape (iterator, no forker, header handling), then take the batching on top and verify an imported db against a known-good head. Tooling only. | upstream boundary `be4dc0c4b`; commit `8f9061f93` (#33894); relates to the batch-12 #32157 EraE row | +| OpenTelemetry SampleRatio IsSet guard (#34062) | batch 19 (v1.17.2 4/4, `be4dc0c4b`) | coverage-gap | Wraps `tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name)` in `if ctx.IsSet(...)` so an unset flag no longer clobbers a configured value. Bor has no `setOpenTelemetry` function at all — the whole OTel flag block was declined with the batch-9/11 OTel rows — so there is nothing to guard. Kept Bor's `cmd/utils/flags.go`. | Re-apply automatically when the OTel line is adopted; it is one `if` around the existing assignment. | upstream boundary `be4dc0c4b`; commit `745b0a8c0` (#34062); depends on the batch-9 #33452 / batch-11 #33484 rows | +| Slot number in test payload (#34094) | batch 19 (v1.17.2 4/4, `be4dc0c4b`) | coverage-gap | Adds `slotNum: args.SlotNum` to the `generateParams` literal inside `miner.BuildTestingPayload`. Bor deleted `BuildTestingPayload` along with the `testing_buildBlockV1` RPC (#33656, declined in batch 12), so the only changed line has no home. Kept Bor's `miner/payload_building.go`. | Re-apply automatically when the #33656 row is adopted; it is one field in the params literal. | upstream boundary `be4dc0c4b`; commit `acdd13971` (#34094); depends on the batch-12 #33656 row | +| Default cache size bump — 1024 MB → 4096 MB (#33836 / #33975) | v1.17.2 milestone triage (batches 16, 17) | **merged clean, no effect on Bor — decision needed** | Upstream raised the total default cache from 1024 MB to 4096 MB, which lands in `eth/ethconfig.Defaults` as `DatabaseCache 512→2048`, `TrieCleanCache 154→614`, `TrieDirtyCache 256→1024`, `SnapshotCache 102→409`. Both commits merged without conflict — but **Bor's production startup path never reads those defaults**. `internal/cli/server/config.go` defines its own `Cache: 1024` with a 50/15/25/10 `PercDatabase`/`PercTrie`/`PercGc`/`PercSnapshot` split, and `calcPerc` overwrites all four `ethconfig` values in `fillEthConfig`. That split against 1024 MB reproduces geth's **old** defaults exactly (512 / 153 / 256 / 102), which is evidently how it was tuned — so upstream's bump silently leaves `bor server` 4× below upstream's new intent while `cmd/geth`, tests, and any `ethconfig.Defaults` consumer get the new values. | One-line change if the team wants parity: `Cache: 1024` → `Cache: 4096` in the `internal/cli/server` defaults (the existing percentage split then reproduces upstream's new numbers exactly, 2048 / 614 / 1024 / 409). **Deliberately not taken here** — it quadruples the default memory footprint of every Bor node, which is a PoS/devops call, not a merge decision. Note that pos-ops pins `cache` per host in each BP's `bor/config.toml`, so deployed BPs are unaffected either way; this only moves the default for operators who don't set it. | upstream boundary `00540f946` (batch 16) and `77e7e5ad1` (batch 17); commits `28dad943f` (#33836), `b8a3fa7d0` (#33975) | + ## Notes - Fork-schedule adoptions (EIP-8024, Verkle, and the upcoming Osaka/BPO/ diff --git a/docs/upstream-merges/v1.17.4/plan.md b/docs/upstream-merges/v1.17.4/plan.md index 3667c276af..3d0f38bb36 100644 --- a/docs/upstream-merges/v1.17.4/plan.md +++ b/docs/upstream-merges/v1.17.4/plan.md @@ -37,10 +37,10 @@ Status legend: `pending` / `in-progress` / `merged` / `skipped`. | 13 | v1.17.0 | 12/12 | `0cf3d3ba4` | 7 | delayed p2p decoding, header-verification hardening, **v1.17.0 release** | merged (`70994671a`) | | 14 | v1.17.1 | 1/2 | `9ecb6c4ae` | 20 | **syscall value-transfer disable (`core/vm`)**, BAL type changes, **Amsterdam precompile touch** | merged (`189030866`) | | 15 | v1.17.1 | 2/2 | `16783c167` | 20 | **eth/68 protocol drop**, **EIP-7843 SLOTNUM**, **8024 enabled in Amsterdam**, Go 1.26; release | merged (`b6175113d`) | -| 16 | v1.17.2 | 1/4 | `00540f946` | 20 | **EIP-7778 block gas accounting**, miner prefetcher, **amsterdam jump table**, default cache 4096 | pending | -| 17 | v1.17.2 | 2/4 | `77e7e5ad1` | 20 | codedb refactor, **EIP-7954 max contract size**, trienode history alongside data | pending | -| 18 | v1.17.2 | 3/4 | `e23b0cbc2` | 20 | stateless codedb fix, **call-variant gas measurement rework (`core/vm`)**, bintrie parallel hash | pending | -| 19 | v1.17.2 | 4/4 | `be4dc0c4b` | 17 | **EIP-7708**, simulateV1/getProofs limits, **v1.17.2 release** | pending | +| 16 | v1.17.2 | 1/4 | `00540f946` | 20 | **EIP-7778 block gas accounting**, miner prefetcher, **amsterdam jump table**, default cache 4096 | merged (`09c784851`) | +| 17 | v1.17.2 | 2/4 | `77e7e5ad1` | 20 | codedb refactor, **EIP-7954 max contract size**, trienode history alongside data | merged (`0a83ed542`) | +| 18 | v1.17.2 | 3/4 | `e23b0cbc2` | 20 | stateless codedb fix, **call-variant gas measurement rework (`core/vm`)**, bintrie parallel hash | merged (`1abb57b8f`) | +| 19 | v1.17.2 | 4/4 | `be4dc0c4b` | 17 | **EIP-7708**, simulateV1/getProofs limits, **v1.17.2 release** | merged (`682b4c380`) | | 20 | v1.17.3 | 1/7 | `04e40995d` | 20 | **eth/70 partial receipts**, BAL storage layer + snap/2 BAL serving | pending | | 21 | v1.17.3 | 2/7 | `c453b99a5` | 20 | **gas becomes vector (`core`)**, bintrie fixes | pending | | 22 | v1.17.3 | 3/7 | `5af5510b1` | 20 | EIP-7610 rework, CachingDB split (merkle/binary), freezer fsync fix | pending | diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 290e9ba3f6..45da0a679f 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -291,12 +291,9 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl if res := api.checkInvalidAncestor(update.HeadBlockHash, update.HeadBlockHash); res != nil { return engine.ForkChoiceResponse{PayloadStatus: *res, PayloadID: nil}, nil } - // If the head hash is unknown (was not given to us in a newPayload request), - // we cannot resolve the header, so not much to do. This could be extended in - // the future to resolve from the `eth` network, but it's an unexpected case - // that should be fixed, not papered over. header := api.remoteBlocks.get(update.HeadBlockHash) if header == nil { + // The head hash is unknown locally, try to resolve it from the `eth` network log.Warn("Fetching the unknown forkchoice head from network", "hash", update.HeadBlockHash) retrievedHead, err := api.eth.Downloader().GetHeader(update.HeadBlockHash) if err != nil { @@ -309,7 +306,9 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // If the finalized hash is known, we can direct the downloader to move // potentially more data to the freezer from the get go. finalized := api.remoteBlocks.get(update.FinalizedBlockHash) - + if finalized == nil { + finalized = api.eth.BlockChain().GetHeaderByHash(update.FinalizedBlockHash) + } // Header advertised via a past newPayload request. Start syncing to it. context := []interface{}{"number", header.Number, "hash", header.Hash()} if update.FinalizedBlockHash != (common.Hash{}) { @@ -479,7 +478,7 @@ func (api *ConsensusAPI) GetPayloadV2(payloadID engine.PayloadID) (*engine.Execu payloadID, false, []engine.PayloadVersion{engine.PayloadV1, engine.PayloadV2}, - []forks.Fork{forks.Shanghai}, + []forks.Fork{forks.Paris, forks.Shanghai}, ) } diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 7c50289eae..eee2c2b27b 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -1319,6 +1319,11 @@ func TestNilWithdrawals(t *testing.T) { Random: test.blockParams.Random, Version: payloadVersion, }).Id() + if !shanghai { + if _, err := api.GetPayloadV2(payloadID); err != nil { + t.Fatalf("GetPayloadV2 rejected pre-shanghai payload: %v", err) + } + } execData, err := api.getPayload(payloadID, false, nil, nil) if err != nil { t.Fatalf("error getting payload, err=%v", err) diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index 25a136f049..3cb21fcb30 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -100,6 +100,8 @@ type SimulatedBeacon struct { func payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion { switch config.LatestFork(time) { + case forks.Amsterdam: + return engine.PayloadV4 case forks.BPO5, forks.BPO4, forks.BPO3, forks.BPO2, forks.BPO1, forks.Osaka, forks.Prague, forks.Cancun: return engine.PayloadV3 case forks.Paris, forks.Shanghai: @@ -194,13 +196,19 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u var random [32]byte rand.Read(random[:]) - fcResponse, err := c.engineAPI.forkchoiceUpdated(c.curForkchoiceState, &engine.PayloadAttributes{ + + attribute := &engine.PayloadAttributes{ Timestamp: timestamp, SuggestedFeeRecipient: feeRecipient, Withdrawals: withdrawals, Random: random, BeaconRoot: &common.Hash{}, - }, version, false) + } + if c.eth.BlockChain().Config().LatestFork(timestamp) == forks.Amsterdam { + slotNumber := uint64(0) + attribute.SlotNumber = &slotNumber + } + fcResponse, err := c.engineAPI.forkchoiceUpdated(c.curForkchoiceState, attribute, version, false) if err != nil { return err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index a139b1ebe2..e967ab424a 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -90,11 +90,11 @@ var Defaults = Config{ StateHistory: params.FullImmutabilityThreshold, TrienodeHistory: -1, NodeFullValueCheckpoint: pathdb.Defaults.FullValueCheckpoint, - DatabaseCache: 512, - TrieCleanCache: 154, - TrieDirtyCache: 256, + DatabaseCache: 2048, + TrieCleanCache: 614, + TrieDirtyCache: 1024, + SnapshotCache: 409, TrieTimeout: 60 * time.Minute, - SnapshotCache: 102, FilterLogCacheSize: 32, LogQueryLimit: 1000, Miner: miner.DefaultConfig, diff --git a/eth/fetcher/block_fetcher_race_test.go b/eth/fetcher/block_fetcher_race_test.go index b7044988f0..82b123d695 100644 --- a/eth/fetcher/block_fetcher_race_test.go +++ b/eth/fetcher/block_fetcher_race_test.go @@ -327,7 +327,7 @@ func TestWitnessManagerConcurrentAccess(t *testing.T) { block := blocks[blockIdx] peer := "peer-broadcast-" + randomString(5) - witness, err := stateless.NewWitness(block.Header(), nil) + witness, err := stateless.NewWitness(block.Header(), nil, false) if err != nil { continue } diff --git a/eth/fetcher/block_fetcher_test.go b/eth/fetcher/block_fetcher_test.go index 62619f6f95..9906c42878 100644 --- a/eth/fetcher/block_fetcher_test.go +++ b/eth/fetcher/block_fetcher_test.go @@ -1020,7 +1020,7 @@ func (f *fetcherTester) makeWitnessFetcher(peer string, blocks map[common.Hash]* // Get the witness for the single hash if block, ok := blocks[hash]; ok { // Create a witness for the block - witness, err := stateless.NewWitness(block.Header(), nil) + witness, err := stateless.NewWitness(block.Header(), nil, false) if err != nil { return } diff --git a/eth/fetcher/witness_manager_test.go b/eth/fetcher/witness_manager_test.go index 4d7948bc63..2f53d846cd 100644 --- a/eth/fetcher/witness_manager_test.go +++ b/eth/fetcher/witness_manager_test.go @@ -27,7 +27,7 @@ func createTestBlock(number uint64) *types.Block { } func createTestWitnessForBlock(block *types.Block) *stateless.Witness { - witness, err := stateless.NewWitness(block.Header(), nil) + witness, err := stateless.NewWitness(block.Header(), nil, false) if err != nil { panic(err) } diff --git a/eth/filters/api.go b/eth/filters/api.go index 5c58d5d561..7991864b5d 100644 --- a/eth/filters/api.go +++ b/eth/filters/api.go @@ -227,11 +227,13 @@ func (api *FilterAPI) NewPendingTransactions(ctx context.Context, fullTx *bool) return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported } - rpcSub := notifier.CreateSubscription() + var ( + rpcSub = notifier.CreateSubscription() + txs = make(chan []*types.Transaction, 128) + pendingTxSub = api.events.SubscribePendingTxs(txs) + ) go func() { - txs := make(chan []*types.Transaction, 128) - pendingTxSub := api.events.SubscribePendingTxs(txs) defer pendingTxSub.Unsubscribe() chainConfig := api.sys.backend.ChainConfig() @@ -302,11 +304,13 @@ func (api *FilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) { return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported } - rpcSub := notifier.CreateSubscription() + var ( + rpcSub = notifier.CreateSubscription() + headers = make(chan *types.Header) + headersSub = api.events.SubscribeNewHeads(headers) + ) go func() { - headers := make(chan *types.Header) - headersSub := api.events.SubscribeNewHeads(headers) defer headersSub.Unsubscribe() for { @@ -620,6 +624,9 @@ func (api *FilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Lo if f.crit.ToBlock != nil { end = f.crit.ToBlock.Int64() } + if begin >= 0 && begin < int64(api.events.backend.HistoryPruningCutoff()) { + return nil, &history.PrunedHistoryError{} + } head := api.sys.backend.CurrentHeader().Number.Uint64() if err := checkBlockRangeLimit(begin, end, head, api.sys.cfg.RangeLimit); err != nil { return nil, err diff --git a/eth/filters/filter.go b/eth/filters/filter.go index a3936993bc..e97a760e12 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -386,7 +386,7 @@ func (f *Filter) rangeLogs(ctx context.Context, firstBlock, lastBlock uint64) ([ } if firstBlock > lastBlock { - return nil, nil + return nil, errInvalidBlockRange } mb := f.sys.backend.NewMatcherBackend() defer mb.Close() diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go index 7b6357e4dc..7718a582a3 100644 --- a/eth/filters/filter_test.go +++ b/eth/filters/filter_test.go @@ -359,7 +359,8 @@ func testFilters(t *testing.T, history uint64, noHistory bool) { want: `[{"address":"0xff00000000000000000000000000000000000000","topics":["0x0000000000000000000000000000000000000000000000000000746f70696333"],"data":"0x","blockNumber":"0x3e7","transactionHash":"0x53e3675800c6908424b61b35a44e51ca4c73ca603e58a65b32c67968b4f42200","transactionIndex":"0x0","blockHash":"0x5d0849b4c67f044531948b9d5ae667de082130f386d1eb3a971f4c4de960a841","blockTimestamp":"0x2706","logIndex":"0x0","removed":false}]`, }, { - f: sys.NewRangeFilter(int64(rpc.LatestBlockNumber), int64(rpc.FinalizedBlockNumber), nil, nil), + f: sys.NewRangeFilter(int64(rpc.LatestBlockNumber), int64(rpc.FinalizedBlockNumber), nil, nil), + err: errInvalidBlockRange.Error(), }, { f: sys.NewRangeFilter(int64(rpc.SafeBlockNumber), int64(rpc.LatestBlockNumber), nil, nil), diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index f0a5f3fd0f..c3fd479aa6 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -267,7 +266,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio evm.Cancel() }() // Execute the call, returning a wrapped error or the result - result, err := core.ApplyMessage(evm, call, new(core.GasPool).AddGas(math.MaxUint64)) + result, err := core.ApplyMessage(evm, call, nil) if vmerr := dirtyState.Error(); vmerr != nil { return nil, vmerr } diff --git a/eth/handler_wit_test.go b/eth/handler_wit_test.go index 9a16548575..367d8d3c78 100644 --- a/eth/handler_wit_test.go +++ b/eth/handler_wit_test.go @@ -482,7 +482,7 @@ func TestWitHandlerHandle(t *testing.T) { header := &types.Header{ Number: big.NewInt(100), } - witness, _ := stateless.NewWitness(header, nil) + witness, _ := stateless.NewWitness(header, nil, false) packet := &wit.NewWitnessPacket{ Witness: witness, diff --git a/eth/peer_test.go b/eth/peer_test.go index 6f7c98d080..c871aed0a9 100644 --- a/eth/peer_test.go +++ b/eth/peer_test.go @@ -73,7 +73,7 @@ func TestRequestWitnesses_Controlling_Max_Concurrent_Calls(t *testing.T) { defer ctrl.Finish() hashToRequest := common.Hash{123} - witness, _ := stateless.NewWitness(&types.Header{}, nil) + witness, _ := stateless.NewWitness(&types.Header{}, nil, false) FillWitnessWithDeterministicRandomState(witness, 10*1024) var witBuf bytes.Buffer witness.EncodeRLP(&witBuf) @@ -184,7 +184,7 @@ func FillWitnessWithDeterministicRandomState(w *stateless.Witness, targetSize in states := map[string][]byte{ string(buf): buf, } - w.AddState(states) + w.AddState(states, common.Hash{}) total += chunkSize } } @@ -566,7 +566,7 @@ func TestSupportsWitness(t *testing.T) { func TestReconstructWitness(t *testing.T) { t.Run("SuccessfulReconstruction", func(t *testing.T) { // Create a test witness and encode it - witness, _ := stateless.NewWitness(&types.Header{Number: big.NewInt(100)}, nil) + witness, _ := stateless.NewWitness(&types.Header{Number: big.NewInt(100)}, nil, false) FillWitnessWithDeterministicRandomState(witness, 5*1024) var buf bytes.Buffer witness.EncodeRLP(&buf) @@ -599,7 +599,7 @@ func TestReconstructWitness(t *testing.T) { t.Run("OutOfOrderPages", func(t *testing.T) { // Create pages out of order - witness, _ := stateless.NewWitness(&types.Header{Number: big.NewInt(100)}, nil) + witness, _ := stateless.NewWitness(&types.Header{Number: big.NewInt(100)}, nil, false) FillWitnessWithDeterministicRandomState(witness, 3*1024) var buf bytes.Buffer witness.EncodeRLP(&buf) @@ -1580,7 +1580,7 @@ func testPeer(t *testing.T) (*ethPeer, *MockWitnessPeer) { // testWitnessData returns RLP-encoded witness bytes for use in mock responses. func testWitnessData(t *testing.T) []byte { t.Helper() - w, _ := stateless.NewWitness(&types.Header{}, nil) + w, _ := stateless.NewWitness(&types.Header{}, nil, false) FillWitnessWithDeterministicRandomState(w, 10*1024) var buf bytes.Buffer w.EncodeRLP(&buf) diff --git a/eth/protocols/wit/peer_test.go b/eth/protocols/wit/peer_test.go index 8ebb9452b8..beea672938 100644 --- a/eth/protocols/wit/peer_test.go +++ b/eth/protocols/wit/peer_test.go @@ -64,7 +64,7 @@ var testContextHeader3 = &types.Header{ func createWitness(context *types.Header, headers []*types.Header) *stateless.Witness { // Create a new witness with the context and set the headers - w, _ := stateless.NewWitness(context, nil) + w, _ := stateless.NewWitness(context, nil, false) w.Headers = headers return w } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index a07d5e5198..a685d8e33d 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -281,7 +281,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, // Not yet the searched for transaction, execute on top of the current state statedb.SetTxContext(tx.Hash(), idx) // nolint : contextcheck - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { release() return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 2c182e78f4..825e057834 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -613,7 +613,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config stateReceiverAddress := api.backend.ChainConfig().Bor.StateReceiverContract _, err = statefull.ApplyStateSyncEvents(ctx, evm, tx, msg, common.HexToAddress(stateReceiverAddress)) } else { - _, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + _, err = core.ApplyMessage(evm, msg, nil) } if err != nil { @@ -794,7 +794,7 @@ txloop: // Generate the next state snapshot fast without tracing msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) statedb.SetTxContext(tx.Hash(), i) - res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + res, err := core.ApplyMessage(evm, msg, nil) if err != nil { failed = err break txloop @@ -912,7 +912,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block stateReceiverAddress := chainConfig.Bor.StateReceiverContract _, err = statefull.ApplyStateSyncEvents(ctx, evm, tx, msg, common.HexToAddress(stateReceiverAddress)) } else { - _, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + _, err = core.ApplyMessage(evm, msg, nil) } if err != nil { return dumps, err @@ -967,7 +967,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block if isStateSync { vmResult, err = statefull.ApplyStateSyncEvents(ctx, evm, tx, msg, stateReceiverAddress) } else { - vmResult, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + vmResult, err = core.ApplyMessage(evm, msg, nil) } if hooks.OnTxEnd != nil { var receipt *types.Receipt @@ -1407,7 +1407,6 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor tracer *Tracer err error timeout = defaultTraceTimeout - usedGas uint64 ) if config == nil { config = &TraceConfig{} @@ -1522,12 +1521,14 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor } // Handle normal transactions - _, err = core.ApplyTransactionWithEVM(message, new(core.GasPool).AddGas(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, &usedGas, evm) + gp := core.NewGasPool(message.GasLimit) + + _, err = core.ApplyTransactionWithEVM(message, gp, statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, 0, fmt.Errorf("tracing failed: %w", err) } result, err := tracer.GetResult() - return result, usedGas, err + return result, gp.Used(), err } // APIs return the collection of RPC services the tracer package offers. diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index fdabe2925c..12c9b9ed47 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -209,7 +209,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block if idx == txIndex { return tx, blockContext, statedb, release, nil } - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 212319680c..375ee543f7 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -144,7 +144,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { } evm := vm.NewEVM(blockContext, logState, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } @@ -245,7 +245,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { if tracer.OnTxStart != nil { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } - _, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + _, err = core.ApplyMessage(evm, msg, nil) if err != nil { b.Fatalf("failed to execute transaction: %v", err) } @@ -405,7 +405,7 @@ func TestInternals(t *testing.T) { t.Fatalf("test %v: failed to create message: %v", tc.name, err) } tc.tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("test %v: failed to execute transaction: %v", tc.name, err) } diff --git a/eth/tracers/internal/tracetest/erc7562_tracer_test.go b/eth/tracers/internal/tracetest/erc7562_tracer_test.go index 818a0c5d24..7ece8e4d6a 100644 --- a/eth/tracers/internal/tracetest/erc7562_tracer_test.go +++ b/eth/tracers/internal/tracetest/erc7562_tracer_test.go @@ -125,7 +125,7 @@ func TestErc7562Tracer(t *testing.T) { } evm := vm.NewEVM(context, logState, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/internal/tracetest/flat_calltrace_test.go b/eth/tracers/internal/tracetest/flat_calltrace_test.go index eaf65997f7..3d3293e86c 100644 --- a/eth/tracers/internal/tracetest/flat_calltrace_test.go +++ b/eth/tracers/internal/tracetest/flat_calltrace_test.go @@ -117,7 +117,7 @@ func flatCallTracerTestRunner(tb testing.TB, tracerName string, filename string, } evm := vm.NewEVM(blockContext, state.StateDB, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { return fmt.Errorf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/internal/tracetest/prestate_test.go b/eth/tracers/internal/tracetest/prestate_test.go index 7720b2f346..501598d1fa 100644 --- a/eth/tracers/internal/tracetest/prestate_test.go +++ b/eth/tracers/internal/tracetest/prestate_test.go @@ -110,7 +110,7 @@ func testPrestateTracer(tracerName string, dirPath string, t *testing.T) { } evm := vm.NewEVM(blockContext, state.StateDB, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go index 6ce857532d..ee0c103c60 100644 --- a/eth/tracers/logger/access_list_tracer.go +++ b/eth/tracers/logger/access_list_tracer.go @@ -18,6 +18,7 @@ package logger import ( "maps" + "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" @@ -86,12 +87,16 @@ func (al accessList) accessList() types.AccessList { acl := make(types.AccessList, 0, len(al)) for addr, slots := range al { - tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}} - for slot := range slots { - tuple.StorageKeys = append(tuple.StorageKeys, slot) + keys := slices.SortedFunc(maps.Keys(slots), common.Hash.Cmp) + // Ensure keys is never nil to avoid JSON serialization issues. + // When slots is empty, slices.SortedFunc returns nil, but JSON marshaling + // will serialize nil slice as null instead of [], which breaks clients + // that expect storageKeys to always be an array. + if keys == nil { + keys = []common.Hash{} } - acl = append(acl, tuple) + acl = append(acl, types.AccessTuple{Address: addr, StorageKeys: keys}) } return acl diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index ee80435adf..9c4fa18a6f 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -92,7 +92,7 @@ func BenchmarkTransactionTraceV2(b *testing.B) { evm.Config.Tracer = tracer snap := state.StateDB.Snapshot() - _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + _, err := core.ApplyMessage(evm, msg, nil) if err != nil { b.Fatal(err) } diff --git a/ethdb/batch.go b/ethdb/batch.go index c65397bec3..567380c853 100644 --- a/ethdb/batch.go +++ b/ethdb/batch.go @@ -39,6 +39,9 @@ type Batch interface { // Replay replays the batch contents. Replay(w KeyValueWriter) error + + // Close closes the batch and releases all associated resources. + Close() } // Batcher wraps the NewBatch method of a backing data store. diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index 255cfec85a..a7d21069d6 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -546,6 +546,9 @@ func (b *batch) Replay(w ethdb.KeyValueWriter) error { return b.b.Replay(&replayer{writer: w}) } +// Close closes the batch and releases all associated resources. +func (b *batch) Close() {} + // replayer is a small wrapper to implement the correct replay methods. type replayer struct { writer ethdb.KeyValueWriter diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index da4936b214..7c333e81dd 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -355,6 +355,9 @@ func (b *batch) Replay(w ethdb.KeyValueWriter) error { return nil } +// Close closes the batch and releases all associated resources. +func (b *batch) Close() {} + // iterator can walk over the (potentially partial) keyspace of a memory key // value store. Internally it is a deep copy of the entire iterated state, // sorted by keys. diff --git a/ethdb/pebble/pebble.go b/ethdb/pebble/pebble.go index 7dd2f3e030..b82e279497 100644 --- a/ethdb/pebble/pebble.go +++ b/ethdb/pebble/pebble.go @@ -936,6 +936,12 @@ func (b *batch) Replay(w ethdb.KeyValueWriter) error { } } +// Close closes the batch and releases all associated resources. After it is +// closed, any subsequent operations on this batch are undefined. +func (b *batch) Close() { + b.b.Close() +} + // pebbleIterator is a wrapper of underlying iterator in storage engine. // The purpose of this structure is to implement the missing APIs. // diff --git a/go.mod b/go.mod index e99c487362..51aec9e482 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/consensys/gnark-crypto v0.19.2 github.com/cosmos/cosmos-sdk v0.50.15 github.com/cosmos/gogoproto v1.7.2 - github.com/crate-crypto/go-eth-kzg v1.4.0 + github.com/crate-crypto/go-eth-kzg v1.5.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dchest/siphash v1.2.3 github.com/deckarep/golang-set/v2 v2.6.0 @@ -63,7 +63,7 @@ require ( github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 github.com/jellydator/ttlcache/v3 v3.4.0 github.com/json-iterator/go v1.1.12 - github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 + github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index f17321ab15..fd5bbb9073 100644 --- a/go.sum +++ b/go.sum @@ -345,8 +345,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/creachadair/atomicfile v0.4.0 h1:umZ+njabCI7eeMLWXFRuUy83iQSiA8qnUj8MH04pfbM= github.com/creachadair/atomicfile v0.4.0/go.mod h1:OyQCzy3n5KhEXHcHxSzZ7BbRt7QmeVkaiMJmtENydDI= github.com/creachadair/mds v0.25.6 h1:l1MkbdJmhqXTpm92VWYTb0/s0wsiZSKKc/VKU0lf2bA= @@ -803,8 +803,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 h1:msKODTL1m0wigztaqILOtla9HeW1ciscYG4xjLtvk5I= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= +github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb h1:Ag83At00qa4FLkcdMgrwHVSakqky/eZczOlxd4q336E= +github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 8feb96725e..332aa67d40 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -64,6 +64,10 @@ const estimateGasErrorRatio = 0.015 // be requested in a single eth_getStorageValues call. const maxGetStorageSlots = 1024 +// maxGetProofKeys is the maximum number of storage keys that can be +// requested in a single eth_getProof call. +const maxGetProofKeys = 1024 + var errBlobTxNotSupported = errors.New("signing blob transactions not supported") var errSubClosed = errors.New("chain subscription closed") @@ -455,6 +459,9 @@ func (n *proofList) Delete(key []byte) error { // GetProof returns the Merkle-proof for a given account and optionally some storage keys. func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) { + if len(storageKeys) > maxGetProofKeys { + return nil, &invalidParamsError{fmt.Sprintf("too many storage keys requested (max %d, got %d)", maxGetProofKeys, len(storageKeys))} + } var ( keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) @@ -486,6 +493,9 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, } // Create the proofs for the storageKeys. for i, key := range keys { + if err := ctx.Err(); err != nil { + return nil, err + } // Output key encoding is a bit special: if the input was a 32-byte hash, it is // returned as such. Otherwise, we apply the QUANTITY encoding mandated by the // JSON-RPC spec for getProof. This behavior exists to preserve backwards @@ -950,11 +960,9 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S if isBorInternalCall(ctx) && isBorSystemTx(b.ChainConfig().Bor, args.To) { globalGasCap = 0 } - gp := new(core.GasPool) + gp := core.NewGasPool(globalGasCap) if globalGasCap == 0 { - gp.AddGas(gomath.MaxUint64) - } else { - gp.AddGas(globalGasCap) + gp = core.NewGasPool(gomath.MaxUint64) } return applyMessage(ctx, b, args, state, header, timeout, globalGasCap, gp, &blockCtx, &vm.Config{NoBaseFee: true}, precompiles) } @@ -1301,7 +1309,7 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { result["requestsHash"] = head.RequestsHash } if head.SlotNumber != nil { - result["slotNumber"] = head.SlotNumber + result["slotNumber"] = hexutil.Uint64(*head.SlotNumber) } return result } @@ -1803,7 +1811,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 { evm.Context.BlobBaseFee = new(big.Int) } - res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + res, err := core.ApplyMessage(evm, msg, nil) if err != nil { return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction(types.LegacyTxType).Hash(), err) } @@ -2854,7 +2862,7 @@ func (api *DebugAPI) AccountAt(ctx context.Context, blockHash common.Hash, txInd } stateDb.SetTxContext(tx.Hash(), int(idx)) - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } diff --git a/internal/ethapi/bor_api_test.go b/internal/ethapi/bor_api_test.go index 03f0ccb3de..d9c4194db8 100644 --- a/internal/ethapi/bor_api_test.go +++ b/internal/ethapi/bor_api_test.go @@ -59,7 +59,7 @@ func TestBorWitnessAPI_Integration(t *testing.T) { require.NotNil(t, testBlock) testBlockHash := testBlock.Hash() - mockWitness, err := stateless.NewWitness(testBlock.Header(), backend.chain) + mockWitness, err := stateless.NewWitness(testBlock.Header(), backend.chain, false) require.NoError(t, err) require.NotNil(t, mockWitness) diff --git a/internal/ethapi/errors.go b/internal/ethapi/errors.go index e406c36d6c..cc79af6f3c 100644 --- a/internal/ethapi/errors.go +++ b/internal/ethapi/errors.go @@ -141,7 +141,7 @@ func txValidationError(err error) *invalidTxError { return &invalidTxError{Message: err.Error(), Code: errCodeIntrinsicGas} case errors.Is(err, core.ErrInsufficientFundsForTransfer): return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds} - case errors.Is(err, core.ErrMaxInitCodeSizeExceeded): + case errors.Is(err, vm.ErrMaxInitCodeSizeExceeded): return &invalidTxError{Message: err.Error(), Code: errCodeMaxInitCodeSizeExceeded} } return &invalidTxError{ diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 11f1e34d34..851f231148 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -272,7 +272,13 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlo return nil, err } headers[bi] = result.Header() - results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults, senders: senders} + results[bi] = &simBlockResult{ + fullTx: sim.fullTx, + chainConfig: sim.chainConfig, + Block: result, + Calls: callResults, + senders: senders, + } parent = result.Header() } return results, nil @@ -306,16 +312,17 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, blockContext.BlobBaseFee = block.BlockOverrides.BlobBaseFee.ToInt() } precompiles := sim.activePrecompiles(header) + // State overrides are applied prior to execution of a block if err := block.StateOverrides.Apply(sim.state, precompiles); err != nil { return nil, nil, nil, err } var ( - gp = new(core.GasPool).AddGas(blockContext.GasLimit) - gasUsed, blobGasUsed uint64 - txes = make([]*types.Transaction, len(block.Calls)) - callResults = make([]simCallResult, len(block.Calls)) - receipts = make([]*types.Receipt, len(block.Calls)) + gp = core.NewGasPool(blockContext.GasLimit) + blobGasUsed uint64 + txes = make([]*types.Transaction, len(block.Calls)) + callResults = make([]simCallResult, len(block.Calls)) + receipts = make([]*types.Receipt, len(block.Calls)) // Block hash will be repaired after execution. tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0) vmConfig = &vm.Config{ @@ -346,6 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } var allLogs []*types.Log for i, call := range block.Calls { + // Terminate if the context is cancelled if err := ctx.Err(); err != nil { return nil, nil, nil, err } @@ -360,8 +368,9 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, txes[i] = tx senders[txHash] = call.from() tracer.reset(txHash, uint(i)) - sim.state.SetTxContext(txHash, i) + // EoA check is always skipped, even in validation mode. + sim.state.SetTxContext(txHash, i) msg := call.ToMessage(header.BaseFee, !sim.validate) result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp) if err != nil { @@ -375,8 +384,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } else { root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes() } - gasUsed += result.UsedGas - receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gasUsed, root) + receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gp.CumulativeUsed(), root) blobGasUsed += receipts[i].BlobGasUsed // Enforce the cross-block gas budget. @@ -405,7 +413,8 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } callResults[i] = callRes } - header.GasUsed = gasUsed + // Assign total consumed gas to the header + header.GasUsed = gp.Used() if sim.chainConfig.IsCancun(header.Number) { header.BlobGasUsed = &blobGasUsed } @@ -591,6 +600,7 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { if sim.chainConfig.IsShanghai(number) && sim.chainConfig.Bor == nil { withdrawalsHash = &types.EmptyWithdrawalsHash } + var parentBeaconRoot *common.Hash if sim.chainConfig.IsCancun(number) { parentBeaconRoot = &common.Hash{} @@ -620,7 +630,11 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { } func (sim *simulator) newSimulatedChainContext(ctx context.Context, headers []*types.Header) *ChainContext { - return NewChainContext(ctx, &simBackend{base: sim.base, b: sim.b, headers: headers}) + return NewChainContext(ctx, &simBackend{ + base: sim.base, + b: sim.b, + headers: headers, + }) } type simBackend struct { diff --git a/miner/payload_building.go b/miner/payload_building.go index 8422d18751..e46dbbc4d3 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -264,6 +264,17 @@ func (w *worker) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload, e for { select { case <-timer.C: + // When block building takes close to the full recommit interval, + // the timer fires near-instantly on the next iteration. If the + // payload was resolved during that build, both timer.C and + // payload.stop are ready and Go's select picks one at random. + // Check payload.stop first to avoid an unnecessary generateWork. + select { + case <-payload.stop: + log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery") + return + default: + } start := time.Now() r := w.generateWork(fullParams, witness) if r.err == nil { diff --git a/miner/worker.go b/miner/worker.go index 55bfe7fe42..36a020b3e6 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -346,7 +346,7 @@ func stateSyncReserveFor(config *params.ChainConfig, number *big.Int) uint64 { return params.MaxStateSyncBytesPerBlock } -// txFits reports whether the transaction fits into the block size limit. +// txFitsSize reports whether the transaction fits into the block size limit. func (env *environment) txFitsSize(tx *types.Transaction) bool { return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone-env.stateSyncReserve } @@ -1417,14 +1417,14 @@ func (w *worker) makeEnv(header *types.Header, coinbase common.Address, witness } if witness { - bundle, err := stateless.NewWitness(header, w.chain) + bundle, err := stateless.NewWitness(header, w.chain, false) if err != nil { return nil, err } - state.StartPrefetcher("miner", bundle, nil) + state.StartPrefetcher("miner", bundle) } else { // todo: @anshalshukla - check if witness is required - state.StartPrefetcher("miner", nil, nil) + state.StartPrefetcher("miner", nil) } // Note the passed coinbase may be different with header.Coinbase. @@ -1471,16 +1471,18 @@ func (w *worker) updateSnapshot(env *environment) { func (w *worker) commitTransaction(env *environment, tx *types.Transaction) ([]*types.Log, error) { var ( snap = env.state.Snapshot() - gp = env.gasPool.Gas() + gp = env.gasPool.Snapshot() ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, &env.header.GasUsed) + receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) - env.gasPool.SetGas(gp) + env.gasPool.Set(gp) return nil, err } + + env.header.GasUsed = env.gasPool.Used() env.txs = append(env.txs, tx) env.receipts = append(env.receipts, receipt) env.tcount++ @@ -1496,7 +1498,7 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac gasLimit := env.header.GasLimit if env.gasPool == nil { - env.gasPool = new(core.GasPool).AddGas(gasLimit) + env.gasPool = core.NewGasPool(gasLimit) } var coalescedLogs []*types.Log @@ -2711,7 +2713,7 @@ func (w *worker) runIdleTxProvider(txsCh chan<- *types.Transaction, header *type filter := w.buildDefaultFilter(header.BaseFee, header.Number) filter.BlobTxs = false - totalGasPool := new(core.GasPool).AddGas(header.GasLimit * idleGasLimitPercent(w.config) / 100) + totalGasPool := core.NewGasPool(header.GasLimit * idleGasLimitPercent(w.config) / 100) localPrefetched := make(map[common.Hash]struct{}) shouldExit := func() bool { @@ -2764,7 +2766,7 @@ func (w *worker) streamIdleBatch( if loopGasLimit > headerGasLimit { loopGasLimit = headerGasLimit } - gaspool := new(core.GasPool).AddGas(loopGasLimit) + gaspool := core.NewGasPool(loopGasLimit) for { ltx, tx := nextViableIdleTx(txs, gaspool, localPrefetched) diff --git a/miner/worker_prefetch_unit_test.go b/miner/worker_prefetch_unit_test.go index 60b451445b..93b844c48a 100644 --- a/miner/worker_prefetch_unit_test.go +++ b/miner/worker_prefetch_unit_test.go @@ -493,7 +493,7 @@ func TestStreamIdleBatch_LocalBudgetEnforced(t *testing.T) { // Cap headerGasLimit at 100k so loopGasLimit = min(totalGasPool, header) = 100k. const headerGasLimit uint64 = 100_000 - totalGasPool := new(core.GasPool).AddGas(10_000_000) + totalGasPool := core.NewGasPool(10_000_000) localPrefetched := map[common.Hash]struct{}{} // Buffer wide enough that the "channel full" early-return never triggers. diff --git a/miner/worker_test.go b/miner/worker_test.go index 8c88ba5b35..3fda4c643c 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -3863,7 +3863,7 @@ func newSizeTestEnv(t *testing.T, w *worker) *environment { }, false) require.NoError(t, err) - env.gasPool = new(core.GasPool).AddGas(env.header.GasLimit) + env.gasPool = core.NewGasPool(env.header.GasLimit) return env } diff --git a/p2p/dial.go b/p2p/dial.go index bd80bd1b24..9a57872b03 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -77,6 +77,7 @@ var ( errJailed = errors.New("peer is jailed") errAlreadyDialing = errors.New("already dialing") errAlreadyConnected = errors.New("already connected") + errPendingInbound = errors.New("peer has pending inbound connection") errRecentlyDialed = errors.New("recently dialed") errNetRestrict = errors.New("not contained in netrestrict list") errNoPort = errors.New("node does not provide TCP port") @@ -105,12 +106,15 @@ type dialScheduler struct { remStaticCh chan *enode.Node addPeerCh chan *conn remPeerCh chan *conn + addPendingCh chan enode.ID + remPendingCh chan enode.ID // Everything below here belongs to loop and // should only be accessed by code on the loop goroutine. - dialing map[enode.ID]*dialTask // active tasks - peers map[enode.ID]struct{} // all connected peers - dialPeers int // current number of dialed peers + dialing map[enode.ID]*dialTask // active tasks + peers map[enode.ID]struct{} // all connected peers + pendingInbound map[enode.ID]struct{} // in-progress inbound connections + dialPeers int // current number of dialed peers // The static map tracks all static dial tasks. The subset of usable static dial tasks // (i.e. those passing checkDial) is kept in staticPool. The scheduler prefers @@ -169,19 +173,22 @@ func (cfg dialConfig) withDefaults() dialConfig { func newDialScheduler(config dialConfig, it enode.Iterator, setupFunc dialSetupFunc) *dialScheduler { cfg := config.withDefaults() d := &dialScheduler{ - dialConfig: cfg, - historyTimer: mclock.NewAlarm(cfg.clock), - setupFunc: setupFunc, - dnsLookupFunc: net.DefaultResolver.LookupNetIP, - dialing: make(map[enode.ID]*dialTask), - static: make(map[enode.ID]*dialTask), - peers: make(map[enode.ID]struct{}), - doneCh: make(chan *dialTask), - nodesIn: make(chan *enode.Node), - addStaticCh: make(chan *enode.Node), - remStaticCh: make(chan *enode.Node), - addPeerCh: make(chan *conn), - remPeerCh: make(chan *conn), + dialConfig: cfg, + historyTimer: mclock.NewAlarm(cfg.clock), + setupFunc: setupFunc, + dnsLookupFunc: net.DefaultResolver.LookupNetIP, + dialing: make(map[enode.ID]*dialTask), + static: make(map[enode.ID]*dialTask), + peers: make(map[enode.ID]struct{}), + pendingInbound: make(map[enode.ID]struct{}), + doneCh: make(chan *dialTask), + nodesIn: make(chan *enode.Node), + addStaticCh: make(chan *enode.Node), + remStaticCh: make(chan *enode.Node), + addPeerCh: make(chan *conn), + remPeerCh: make(chan *conn), + addPendingCh: make(chan enode.ID), + remPendingCh: make(chan enode.ID), } d.lastStatsLog = d.clock.Now() d.ctx, d.cancel = context.WithCancel(context.Background()) @@ -231,6 +238,22 @@ func (d *dialScheduler) peerRemoved(c *conn) { } } +// inboundPending notifies the scheduler about a pending inbound connection. +func (d *dialScheduler) inboundPending(id enode.ID) { + select { + case d.addPendingCh <- id: + case <-d.ctx.Done(): + } +} + +// inboundCompleted notifies the scheduler that an inbound connection completed or failed. +func (d *dialScheduler) inboundCompleted(id enode.ID) { + select { + case d.remPendingCh <- id: + case <-d.ctx.Done(): + } +} + // loop is the main loop of the dialer. func (d *dialScheduler) loop(it enode.Iterator) { var ( @@ -284,6 +307,15 @@ loop: delete(d.peers, c.node.ID()) d.updateStaticPool(c.node.ID()) + case id := <-d.addPendingCh: + d.pendingInbound[id] = struct{}{} + d.log.Trace("Marked node as pending inbound", "id", id) + + case id := <-d.remPendingCh: + delete(d.pendingInbound, id) + d.updateStaticPool(id) + d.log.Trace("Unmarked node as pending inbound", "id", id) + case node := <-d.addStaticCh: id := node.ID() _, exists := d.static[id] @@ -414,6 +446,9 @@ func (d *dialScheduler) checkDial(n *enode.Node) error { if _, ok := d.peers[n.ID()]; ok { return errAlreadyConnected } + if _, ok := d.pendingInbound[n.ID()]; ok { + return errPendingInbound + } if d.netRestrict != nil && !d.netRestrict.ContainsAddr(n.IPAddr()) { return errNetRestrict } diff --git a/p2p/dial_test.go b/p2p/dial_test.go index 9bd23f055c..d7b761951e 100644 --- a/p2p/dial_test.go +++ b/p2p/dial_test.go @@ -424,6 +424,82 @@ func TestDialSchedDNSHostname(t *testing.T) { }) } +// This test checks that nodes with pending inbound connections are not dialed. +func TestDialSchedPendingInbound(t *testing.T) { + t.Parallel() + + config := dialConfig{ + maxActiveDials: 5, + maxDialPeers: 4, + } + runDialTest(t, config, []dialTestRound{ + // 2 peers are connected, leaving 2 dial slots. + // Node 0x03 has a pending inbound connection. + // Discovered nodes 0x03, 0x04, 0x05 but only 0x04 and 0x05 should be dialed. + { + peersAdded: []*conn{ + {flags: dynDialedConn, node: newNode(uintID(0x01), "127.0.0.1:30303")}, + {flags: dynDialedConn, node: newNode(uintID(0x02), "127.0.0.2:30303")}, + }, + update: func(d *dialScheduler) { + d.inboundPending(uintID(0x03)) + }, + discovered: []*enode.Node{ + newNode(uintID(0x03), "127.0.0.3:30303"), // not dialed because pending inbound + newNode(uintID(0x04), "127.0.0.4:30303"), + newNode(uintID(0x05), "127.0.0.5:30303"), + }, + wantNewDials: []*enode.Node{ + newNode(uintID(0x04), "127.0.0.4:30303"), + newNode(uintID(0x05), "127.0.0.5:30303"), + }, + }, + // Pending inbound connection for 0x03 completes successfully. + // Node 0x03 becomes a connected peer. + // One dial slot remains, node 0x06 is dialed. + { + update: func(d *dialScheduler) { + // Pending inbound completes + d.inboundCompleted(uintID(0x03)) + }, + peersAdded: []*conn{ + {flags: inboundConn, node: newNode(uintID(0x03), "127.0.0.3:30303")}, + }, + succeeded: []enode.ID{ + uintID(0x04), + }, + failed: []enode.ID{ + uintID(0x05), + }, + discovered: []*enode.Node{ + newNode(uintID(0x03), "127.0.0.3:30303"), // not dialed, now connected + newNode(uintID(0x06), "127.0.0.6:30303"), + }, + wantNewDials: []*enode.Node{ + newNode(uintID(0x06), "127.0.0.6:30303"), + }, + }, + // Inbound peer 0x03 disconnects. + // Another pending inbound starts for 0x07. + // Only 0x03 should be dialed, not 0x07. + { + peersRemoved: []enode.ID{ + uintID(0x03), + }, + update: func(d *dialScheduler) { + d.inboundPending(uintID(0x07)) + }, + discovered: []*enode.Node{ + newNode(uintID(0x03), "127.0.0.3:30303"), + newNode(uintID(0x07), "127.0.0.7:30303"), // not dialed because pending inbound + }, + wantNewDials: []*enode.Node{ + newNode(uintID(0x03), "127.0.0.3:30303"), + }, + }, + }) +} + // ------- // Code below here is the framework for the tests above. diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 113bee13df..5167cc8590 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -25,6 +25,7 @@ package discover import ( "context" "fmt" + "net" "net/netip" "slices" "sync" @@ -36,6 +37,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/netutil" ) @@ -207,6 +209,13 @@ func (tab *Table) close() { func (tab *Table) setFallbackNodes(nodes []*enode.Node) error { nursery := make([]*enode.Node, 0, len(nodes)) for _, n := range nodes { + if n.Hostname() != "" && !n.IPAddr().IsValid() { + resolved, err := resolveBootnodeHostname(n, tab.log) + if err != nil { + return fmt.Errorf("bad bootstrap node %q: %v", n, err) + } + n = resolved + } if err := n.ValidateComplete(); err != nil { return fmt.Errorf("bad bootstrap node %q: %v", n, err) } @@ -220,6 +229,42 @@ func (tab *Table) setFallbackNodes(nodes []*enode.Node) error { return nil } +// resolveBootnodeHostname resolves the DNS hostname of a bootstrap node to an IP address. +func resolveBootnodeHostname(n *enode.Node, logger log.Logger) (*enode.Node, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", n.Hostname()) + if err != nil { + return nil, fmt.Errorf("DNS lookup failed for %q: %v", n.Hostname(), err) + } + + var ip4, ip6 netip.Addr + for _, ip := range ips { + if ip.Is4() && !ip4.IsValid() { + ip4 = ip + } + if ip.Is6() && !ip6.IsValid() { + ip6 = ip + } + } + if !ip4.IsValid() && !ip6.IsValid() { + return nil, fmt.Errorf("no IP addresses found for hostname %q", n.Hostname()) + } + + rec := n.Record() + if ip4.IsValid() { + rec.Set(enr.IPv4Addr(ip4)) + } + if ip6.IsValid() { + rec.Set(enr.IPv6Addr(ip6)) + } + rec.SetSeq(n.Seq()) + resolved := enode.SignNull(rec, n.ID()).WithHostname(n.Hostname()) + logger.Debug("Resolved bootstrap node hostname", "name", n.Hostname(), "ip", resolved.IP()) + return resolved, nil +} + // isInitDone returns whether the table's initial seeding procedure has completed. func (tab *Table) isInitDone() bool { select { diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index fc9ecd83b1..c0b1c15b17 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -513,6 +513,66 @@ func quickcfg() *quick.Config { } } +func TestSetFallbackNodes_DNSHostname(t *testing.T) { + // Create a node with a DNS hostname but no IP, simulating an enode URL + // like enode://@localhost:30303. + key := newkey() + node := enode.NewV4(&key.PublicKey, nil, 30303, 30303).WithHostname("localhost") + + // Verify the node has a hostname but no valid IP. + if node.Hostname() != "localhost" { + t.Fatal("expected hostname to be set") + } + if node.IPAddr().IsValid() { + t.Fatal("expected no IP address") + } + + // Create a table and set the hostname node as a bootnode. + // This should resolve the hostname to an IP address. + db, _ := enode.OpenDB(t.TempDir() + "/node.db") + defer db.Close() + + cfg := Config{Log: testlog.Logger(t, log.LvlTrace)} + cfg = cfg.withDefaults() + tab := &Table{ + cfg: cfg, + log: cfg.Log, + refreshReq: make(chan chan struct{}), + revalResponseCh: make(chan revalidationResponse), + addNodeCh: make(chan addNodeOp), + addNodeHandled: make(chan bool), + trackRequestCh: make(chan trackRequestOp), + initDone: make(chan struct{}), + closeReq: make(chan struct{}), + closed: make(chan struct{}), + ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit}, + } + for i := range tab.buckets { + tab.buckets[i] = &bucket{ + index: i, + ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit}, + } + } + + err := tab.setFallbackNodes([]*enode.Node{node}) + if err != nil { + t.Fatalf("setFallbackNodes failed: %v", err) + } + if len(tab.nursery) != 1 { + t.Fatalf("expected 1 nursery node, got %d", len(tab.nursery)) + } + + // The resolved node should have a valid IP and retain the hostname. + resolved := tab.nursery[0] + if !resolved.IPAddr().IsValid() { + t.Fatal("expected resolved node to have a valid IP") + } + if resolved.Hostname() != "localhost" { + t.Errorf("expected hostname to be preserved, got %q", resolved.Hostname()) + } + t.Logf("resolved localhost to %v", resolved.IPAddr()) +} + func newkey() *ecdsa.PrivateKey { key, err := crypto.GenerateKey() if err != nil { diff --git a/p2p/server.go b/p2p/server.go index 5e12773230..0153106dba 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -849,8 +849,11 @@ running: // Ensure that the trusted flag is set before checking against MaxPeers. c.flags |= trustedConn } - // TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them. - c.cont <- srv.postHandshakeChecks(peers, inboundCount, c) + err := srv.postHandshakeChecks(peers, inboundCount, c) + if err == nil && c.flags&inboundConn != 0 { + srv.dialsched.inboundPending(c.node.ID()) + } + c.cont <- err case c := <-srv.checkpointAddPeer: // At this point the connection is past the protocol handshake. @@ -1052,6 +1055,11 @@ func (srv *Server) checkInboundConn(remoteIP netip.Addr) error { // or the handshakes have failed. func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) error { c := &conn{fd: fd, flags: flags, cont: make(chan error)} + defer func() { + if c.is(inboundConn) && c.node != nil { + srv.dialsched.inboundCompleted(c.node.ID()) + } + }() if dialDest == nil { c.transport = srv.newTransport(fd, nil) } else { diff --git a/params/protocol_params.go b/params/protocol_params.go index 7f8cd458ad..efe2a98ccb 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -153,9 +153,11 @@ const ( DefaultTargetGasPercentage = 50 // Specifies target block gas as percentage of block gas limit for EIP-1559 TargetGasPercentagePostDandeli = 65 // Specifies target block gas as percentage of block gas limit for EIP-1559 after Dandeli hard fork - MaxCodeSize = 24576 // Maximum bytecode to permit for a contract - MaxCodeSizePostAhmedabad = 32768 // Maximum bytecode to permit for a contract post Ahmedabad hard fork (bor / polygon pos) (32KB) - MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions + MaxCodeSize = 24576 // Maximum bytecode to permit for a contract + MaxCodeSizePostAhmedabad = 32768 // Maximum bytecode to permit for a contract post Ahmedabad hard fork (bor / polygon pos) (32KB) + MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions + MaxCodeSizeAmsterdam = 32768 // Maximum bytecode to permit for a contract post Amsterdam (EIP-7954) + MaxInitCodeSizeAmsterdam = 2 * MaxCodeSizeAmsterdam // Maximum initcode to permit in a creation transaction and create instructions post Amsterdam // Precompiled contract gas prices @@ -321,3 +323,10 @@ func BaseFeeChangeDenominator(borConfig *BorConfig, number *big.Int) uint64 { } return DefaultBaseFeeChangeDenominator } + +// System log events. +var ( + // EIP-7708 - System logs emitted for ETH transfer and burn + EthTransferLogEvent = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") // keccak256('Transfer(address,address,uint256)') + EthBurnLogEvent = common.HexToHash("0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5") // keccak256('Burn(address,uint256)') +) diff --git a/rlp/encbuffer.go b/rlp/encbuffer.go index a0d4f2ab15..78b8fcf871 100644 --- a/rlp/encbuffer.go +++ b/rlp/encbuffer.go @@ -392,6 +392,16 @@ func (w *EncoderBuffer) AppendToBytes(dst []byte) []byte { return out } +// Size returns the total size of the content that was encoded up to this point. +// Note this does not count the size of any lists which are still 'open' (i.e. for +// which ListEnd has not been called yet). +func (w EncoderBuffer) Size() int { + if w.buf == nil { + return 0 + } + return w.buf.size() +} + // Write appends b directly to the encoder output. func (w EncoderBuffer) Write(b []byte) (int, error) { return w.buf.Write(b) diff --git a/rlp/encode_test.go b/rlp/encode_test.go index 53c6e7eec3..b5c052f0fa 100644 --- a/rlp/encode_test.go +++ b/rlp/encode_test.go @@ -524,6 +524,39 @@ func TestEncodeToReaderReturnToPool(t *testing.T) { wg.Wait() } +func TestEncoderBufferSize(t *testing.T) { + var output bytes.Buffer + eb := NewEncoderBuffer(&output) + + assertSize := func(state string, expectedSize int) { + t.Helper() + if s := eb.Size(); s != expectedSize { + t.Fatalf("wrong size %s: %d", state, s) + } + } + + assertSize("empty buffer", 0) + outerList := eb.List() + assertSize("after outer List()", 0) + eb.WriteString("abc") + assertSize("after string write", 4) + innerList := eb.List() + assertSize("after inner List()", 4) + eb.WriteUint64(1) + eb.WriteUint64(2) + assertSize("after inner list writes", 6) + eb.ListEnd(innerList) + assertSize("after end of inner list", 7) + eb.ListEnd(outerList) + assertSize("after end of outer list", 8) + eb.Flush() + assertSize("after Flush()", 0) + + if output.Len() != 8 { + t.Fatalf("wrong final output size %d", output.Len()) + } +} + var sink interface{} func BenchmarkIntsize(b *testing.B) { diff --git a/rlp/raw.go b/rlp/raw.go index 782da83d05..f092730779 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -168,6 +168,18 @@ func (r *RawList[T]) AppendRaw(b []byte) error { return nil } +// AppendList appends all items from another RawList to this list. +func (r *RawList[T]) AppendList(other *RawList[T]) { + if other.enc == nil || other.length == 0 { + return + } + if r.enc == nil { + r.enc = make([]byte, 9) + } + r.enc = append(r.enc, other.Content()...) + r.length += other.length +} + // StringSize returns the encoded size of a string. func StringSize(s string) uint64 { switch n := len(s); n { diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 7d544c3577..c805f609f8 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -246,6 +246,54 @@ func TestRawListAppendRaw(t *testing.T) { t.Fatalf("wrong Len %d after invalid appends, want 2", rl.Len()) } } +func TestRawListAppendList(t *testing.T) { + var rl1 RawList[uint64] + if err := rl1.Append(uint64(1)); err != nil { + t.Fatal("append 1 failed:", err) + } + if err := rl1.Append(uint64(2)); err != nil { + t.Fatal("append 2 failed:", err) + } + + var rl2 RawList[uint64] + if err := rl2.Append(uint64(3)); err != nil { + t.Fatal("append 3 failed:", err) + } + if err := rl2.Append(uint64(4)); err != nil { + t.Fatal("append 4 failed:", err) + } + + rl1.AppendList(&rl2) + + if rl1.Len() != 4 { + t.Fatalf("wrong Len %d, want 4", rl1.Len()) + } + if rl1.Size() != 5 { + t.Fatalf("wrong Size %d, want 5", rl1.Size()) + } + + items, err := rl1.Items() + if err != nil { + t.Fatal("Items failed:", err) + } + if !reflect.DeepEqual(items, []uint64{1, 2, 3, 4}) { + t.Fatalf("wrong items: %v", items) + } + + var empty RawList[uint64] + prevLen := rl1.Len() + rl1.AppendList(&empty) + + if rl1.Len() != prevLen { + t.Fatalf("appending empty list changed Len: got %d, want %d", rl1.Len(), prevLen) + } + + empty.AppendList(&rl1) + + if empty.Len() != 4 { + t.Fatalf("wrong Len %d, want 4", empty.Len()) + } +} func TestRawListDecodeInvalid(t *testing.T) { tests := []struct { diff --git a/signer/core/uiapi.go b/signer/core/uiapi.go index 43e28d67c6..1297d7efd7 100644 --- a/signer/core/uiapi.go +++ b/signer/core/uiapi.go @@ -74,8 +74,9 @@ type rawWallet struct { // Example call // {"jsonrpc":"2.0","method":"clef_listWallets","params":[], "id":5} func (api *UIServerAPI) ListWallets() []rawWallet { - wallets := make([]rawWallet, 0) // return [] instead of nil if empty - for _, wallet := range api.am.Wallets() { + allWallets := api.am.Wallets() + wallets := make([]rawWallet, 0, len(allWallets)) // return [] instead of nil if empty + for _, wallet := range allWallets { status, failure := wallet.Status() raw := rawWallet{ @@ -138,8 +139,12 @@ func (api *UIServerAPI) ImportRawKey(privkey string, password string) (accounts. if err := ValidatePasswordFormat(password); err != nil { return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err) } + ks := fetchKeystore(api.am) + if ks == nil { + return accounts.Account{}, errors.New("password based accounts not supported") + } // No error - return fetchKeystore(api.am).ImportECDSA(key, password) + return ks.ImportECDSA(key, password) } // OpenWallet initiates a hardware wallet opening procedure, establishing a USB diff --git a/tests/bor/helper.go b/tests/bor/helper.go index 3ce8c4737f..01c7614922 100644 --- a/tests/bor/helper.go +++ b/tests/bor/helper.go @@ -355,7 +355,7 @@ func (b *blockGen) addTxWithChain(bc *core.BlockChain, statedb *state.StateDB, t context := core.NewEVMBlockContext(b.header, bc, nil) evm := vm.NewEVM(context, statedb, bc.Config(), vm.Config{}) - receipt, err := core.ApplyTransaction(evm, b.gasPool, statedb, b.header, tx, &b.header.GasUsed) + receipt, err := core.ApplyTransaction(evm, b.gasPool, statedb, b.header, tx) if err != nil { panic(err) } @@ -374,7 +374,7 @@ func (b *blockGen) setCoinbase(addr common.Address) { } b.header.Coinbase = addr - b.gasPool = new(core.GasPool).AddGas(b.header.GasLimit) + b.gasPool = core.NewGasPool(b.header.GasLimit) } func sign(t *testing.T, header *types.Header, signer []byte, c *params.BorConfig) { diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 7ca6dc3772..894af77d38 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -360,9 +360,7 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh } // Execute the message. snapshot := st.StateDB.Snapshot() - gaspool := new(core.GasPool) - gaspool.AddGas(block.GasLimit()) - vmRet, err := core.ApplyMessage(evm, msg, gaspool) + vmRet, err := core.ApplyMessage(evm, msg, core.NewGasPool(block.GasLimit())) if err != nil { st.StateDB.RevertToSnapshot(snapshot) if tracer := evm.Config.Tracer; tracer != nil && tracer.OnTxEnd != nil { diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go index 690489b2aa..a7392ec958 100644 --- a/trie/bintrie/binary_node.go +++ b/trie/bintrie/binary_node.go @@ -90,8 +90,18 @@ func SerializeNode(node BinaryNode) []byte { var invalidSerializedLength = errors.New("invalid serialized node length") -// DeserializeNode deserializes a binary trie node from a byte slice. +// DeserializeNode deserializes a binary trie node from a byte slice. The +// hash will be recomputed from the deserialized data. func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) { + return deserializeNode(serialized, depth, common.Hash{}, true) +} + +// DeserializeNodeWithHash deserializes a binary trie node from a byte slice, using the provided hash. +func DeserializeNodeWithHash(serialized []byte, depth int, hn common.Hash) (BinaryNode, error) { + return deserializeNode(serialized, depth, hn, false) +} + +func deserializeNode(serialized []byte, depth int, hn common.Hash, mustRecompute bool) (BinaryNode, error) { if len(serialized) == 0 { return Empty{}, nil } @@ -102,9 +112,11 @@ func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) { return nil, invalidSerializedLength } return &InternalNode{ - depth: depth, - left: HashedNode(common.BytesToHash(serialized[1:33])), - right: HashedNode(common.BytesToHash(serialized[33:65])), + depth: depth, + left: HashedNode(common.BytesToHash(serialized[1:33])), + right: HashedNode(common.BytesToHash(serialized[33:65])), + hash: hn, + mustRecompute: mustRecompute, }, nil case nodeTypeStem: if len(serialized) < 64 { @@ -124,9 +136,11 @@ func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) { } } return &StemNode{ - Stem: serialized[NodeTypeBytes : NodeTypeBytes+StemSize], - Values: values[:], - depth: depth, + Stem: serialized[NodeTypeBytes : NodeTypeBytes+StemSize], + Values: values[:], + depth: depth, + hash: hn, + mustRecompute: mustRecompute, }, nil default: return nil, errors.New("invalid node type") diff --git a/trie/bintrie/empty.go b/trie/bintrie/empty.go index 7cfe373b35..252146a4a7 100644 --- a/trie/bintrie/empty.go +++ b/trie/bintrie/empty.go @@ -32,9 +32,10 @@ func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (Bi var values [256][]byte values[key[31]] = value return &StemNode{ - Stem: slices.Clone(key[:31]), - Values: values[:], - depth: depth, + Stem: slices.Clone(key[:31]), + Values: values[:], + depth: depth, + mustRecompute: true, }, nil } @@ -53,9 +54,10 @@ func (e Empty) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) { func (e Empty) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) { return &StemNode{ - Stem: slices.Clone(key[:31]), - Values: values, - depth: depth, + Stem: slices.Clone(key[:31]), + Values: values, + depth: depth, + mustRecompute: true, }, nil } diff --git a/trie/bintrie/hashed_node.go b/trie/bintrie/hashed_node.go index e4d8c2e7ac..e44c6d1e8a 100644 --- a/trie/bintrie/hashed_node.go +++ b/trie/bintrie/hashed_node.go @@ -64,7 +64,7 @@ func (h HashedNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver No } // Step 3: Deserialize the resolved data into a concrete node - node, err := DeserializeNode(data, depth) + node, err := DeserializeNodeWithHash(data, depth, common.Hash(h)) if err != nil { return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err) } diff --git a/trie/bintrie/hasher.go b/trie/bintrie/hasher.go new file mode 100644 index 0000000000..b81c145723 --- /dev/null +++ b/trie/bintrie/hasher.go @@ -0,0 +1,39 @@ +// Copyright 2026 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 bintrie + +import ( + "crypto/sha256" + "hash" + "sync" +) + +var sha256Pool = sync.Pool{ + New: func() any { + return sha256.New() + }, +} + +func newSha256() hash.Hash { + h := sha256Pool.Get().(hash.Hash) + h.Reset() + return h +} + +func returnSha256(h hash.Hash) { + sha256Pool.Put(h) +} diff --git a/trie/bintrie/internal_node.go b/trie/bintrie/internal_node.go index 0a7bece521..946203bcfb 100644 --- a/trie/bintrie/internal_node.go +++ b/trie/bintrie/internal_node.go @@ -20,10 +20,30 @@ import ( "crypto/sha256" "errors" "fmt" + "math/bits" + "runtime" + "sync" "github.com/ethereum/go-ethereum/common" ) +// parallelDepth returns the tree depth below which Hash() spawns goroutines. +func parallelDepth() int { + return min(bits.Len(uint(runtime.NumCPU())), 8) +} + +// isDirty reports whether a BinaryNode child needs rehashing. +func isDirty(n BinaryNode) bool { + switch v := n.(type) { + case *InternalNode: + return v.mustRecompute + case *StemNode: + return v.mustRecompute + default: + return false + } +} + func keyToPath(depth int, key []byte) ([]byte, error) { if depth > 31*8 { return nil, errors.New("node too deep") @@ -40,6 +60,9 @@ func keyToPath(depth int, key []byte) ([]byte, error) { type InternalNode struct { left, right BinaryNode depth int + + mustRecompute bool // true if the hash needs to be recomputed + hash common.Hash // cached hash when mustRecompute == false } // GetValuesAtStem retrieves the group of values located at the given stem key. @@ -59,7 +82,7 @@ func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([ if err != nil { return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err) } - node, err := DeserializeNode(data, bt.depth+1) + node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn)) if err != nil { return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err) } @@ -77,7 +100,7 @@ func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([ if err != nil { return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err) } - node, err := DeserializeNode(data, bt.depth+1) + node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn)) if err != nil { return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err) } @@ -108,15 +131,45 @@ func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn // Copy creates a deep copy of the node. func (bt *InternalNode) Copy() BinaryNode { return &InternalNode{ - left: bt.left.Copy(), - right: bt.right.Copy(), - depth: bt.depth, + left: bt.left.Copy(), + right: bt.right.Copy(), + depth: bt.depth, + mustRecompute: bt.mustRecompute, + hash: bt.hash, } } // Hash returns the hash of the node. func (bt *InternalNode) Hash() common.Hash { - h := sha256.New() + if !bt.mustRecompute { + return bt.hash + } + + // At shallow depths, parallelize when both children need rehashing: + // hash left subtree in a goroutine, right subtree inline, then combine. + // Skip goroutine overhead when only one child is dirty (common case + // for narrow state updates that touch a single path through the trie). + if bt.depth < parallelDepth() && isDirty(bt.left) && isDirty(bt.right) { + var input [64]byte + var lh common.Hash + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + lh = bt.left.Hash() + }() + rh := bt.right.Hash() + copy(input[32:], rh[:]) + wg.Wait() + copy(input[:32], lh[:]) + bt.hash = sha256.Sum256(input[:]) + bt.mustRecompute = false + return bt.hash + } + + // Deeper nodes: sequential using pooled hasher (goroutine overhead > hash cost) + h := newSha256() + defer returnSha256(h) if bt.left != nil { h.Write(bt.left.Hash().Bytes()) } else { @@ -127,7 +180,9 @@ func (bt *InternalNode) Hash() common.Hash { } else { h.Write(zero[:]) } - return common.BytesToHash(h.Sum(nil)) + bt.hash = common.BytesToHash(h.Sum(nil)) + bt.mustRecompute = false + return bt.hash } // InsertValuesAtStem inserts a full value group at the given stem in the internal node. @@ -149,7 +204,7 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve if err != nil { return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err) } - node, err := DeserializeNode(data, bt.depth+1) + node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn)) if err != nil { return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err) } @@ -157,6 +212,7 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve } bt.left, err = bt.left.InsertValuesAtStem(stem, values, resolver, depth+1) + bt.mustRecompute = true return bt, err } @@ -173,7 +229,7 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve if err != nil { return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err) } - node, err := DeserializeNode(data, bt.depth+1) + node, err := DeserializeNodeWithHash(data, bt.depth+1, common.Hash(hn)) if err != nil { return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err) } @@ -181,6 +237,7 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve } bt.right, err = bt.right.InsertValuesAtStem(stem, values, resolver, depth+1) + bt.mustRecompute = true return bt, err } diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go index 158d8b7147..69097483fd 100644 --- a/trie/bintrie/internal_node_test.go +++ b/trie/bintrie/internal_node_test.go @@ -239,6 +239,7 @@ func TestInternalNodeHash(t *testing.T) { // Changing a child should change the hash node.left = HashedNode(common.HexToHash("0x3333")) + node.mustRecompute = true hash3 := node.Hash() if hash1 == hash3 { t.Error("Hash didn't change after modifying left child") @@ -246,9 +247,10 @@ func TestInternalNodeHash(t *testing.T) { // Test with nil children (should use zero hash) nodeWithNil := &InternalNode{ - depth: 0, - left: nil, - right: HashedNode(common.HexToHash("0x4444")), + depth: 0, + left: nil, + right: HashedNode(common.HexToHash("0x4444")), + mustRecompute: true, } hashWithNil := nodeWithNil.Hash() if hashWithNil == (common.Hash{}) { diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go index 9b863ed1e3..048d37f766 100644 --- a/trie/bintrie/iterator.go +++ b/trie/bintrie/iterator.go @@ -119,27 +119,43 @@ func (it *binaryNodeIterator) Next(descend bool) bool { return it.Next(descend) case HashedNode: // resolve the node - data, err := it.trie.nodeResolver(it.Path(), common.Hash(node)) + resolverPath := it.Path() + data, err := it.trie.nodeResolver(resolverPath, common.Hash(node)) if err != nil { panic(err) } - it.current, err = DeserializeNode(data, len(it.stack)-1) + if data == nil { + // Empty/nil node — treat as Empty, backtrack + it.current = Empty{} + it.stack[len(it.stack)-1].Node = it.current + return it.Next(descend) + } + it.current, err = DeserializeNodeWithHash(data, len(it.stack)-1, common.Hash(node)) if err != nil { panic(err) } // update the stack and parent with the resolved node it.stack[len(it.stack)-1].Node = it.current - parent := &it.stack[len(it.stack)-2] - if parent.Index == 0 { - parent.Node.(*InternalNode).left = it.current - } else { - parent.Node.(*InternalNode).right = it.current + if len(it.stack) >= 2 { + parent := &it.stack[len(it.stack)-2] + if parent.Index == 0 { + parent.Node.(*InternalNode).left = it.current + } else { + parent.Node.(*InternalNode).right = it.current + } } return it.Next(descend) case Empty: - // do nothing - return false + // Empty node - go back to parent and continue + if len(it.stack) <= 1 { + it.lastErr = errIteratorEnd + return false + } + it.stack = it.stack[:len(it.stack)-1] + it.current = it.stack[len(it.stack)-1].Node + it.stack[len(it.stack)-1].Index++ + return it.Next(descend) default: panic("invalid node type") } diff --git a/trie/bintrie/iterator_test.go b/trie/bintrie/iterator_test.go new file mode 100644 index 0000000000..3e717c07ba --- /dev/null +++ b/trie/bintrie/iterator_test.go @@ -0,0 +1,239 @@ +// Copyright 2026 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 bintrie + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/trie" +) + +// makeTrie creates a BinaryTrie populated with the given key-value pairs. +func makeTrie(t *testing.T, entries [][2]common.Hash) *BinaryTrie { + t.Helper() + tr := &BinaryTrie{ + root: NewBinaryNode(), + tracer: trie.NewPrevalueTracer(), + } + for _, kv := range entries { + var err error + tr.root, err = tr.root.Insert(kv[0][:], kv[1][:], nil, 0) + if err != nil { + t.Fatal(err) + } + } + return tr +} + +// countLeaves iterates the trie and returns the number of leaves visited. +func countLeaves(t *testing.T, tr *BinaryTrie) int { + t.Helper() + it, err := newBinaryNodeIterator(tr, nil) + if err != nil { + t.Fatal(err) + } + leaves := 0 + for it.Next(true) { + if it.Leaf() { + leaves++ + } + } + if it.Error() != nil { + t.Fatalf("iterator error: %v", it.Error()) + } + return leaves +} + +// TestIteratorEmptyTrie verifies that iterating over an empty trie returns +// no nodes and reports no error. +func TestIteratorEmptyTrie(t *testing.T) { + tr := &BinaryTrie{ + root: Empty{}, + tracer: trie.NewPrevalueTracer(), + } + it, err := newBinaryNodeIterator(tr, nil) + if err != nil { + t.Fatal(err) + } + if it.Next(true) { + t.Fatal("expected no iteration over empty trie") + } + if it.Error() != nil { + t.Fatalf("unexpected error: %v", it.Error()) + } +} + +// TestIteratorSingleStem verifies iteration over a trie with a single stem +// node containing multiple values. +func TestIteratorSingleStem(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003"), oneKey}, + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000007"), oneKey}, + {common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF"), oneKey}, + }) + if leaves := countLeaves(t, tr); leaves != 3 { + t.Fatalf("expected 3 leaves, got %d", leaves) + } +} + +// TestIteratorTwoStems verifies iteration over a trie with two stems +// separated by internal nodes, ensuring all leaves from both stems are visited. +func TestIteratorTwoStems(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002"), oneKey}, + {common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + {common.HexToHash("8000000000000000000000000000000000000000000000000000000000000002"), oneKey}, + }) + if leaves := countLeaves(t, tr); leaves != 4 { + t.Fatalf("expected 4 leaves, got %d", leaves) + } +} + +// TestIteratorLeafKeyAndBlob verifies that the iterator returns correct +// leaf keys and values. +func TestIteratorLeafKeyAndBlob(t *testing.T) { + key := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005") + val := common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef") + tr := makeTrie(t, [][2]common.Hash{{key, val}}) + + it, err := newBinaryNodeIterator(tr, nil) + if err != nil { + t.Fatal(err) + } + + found := false + for it.Next(true) { + if it.Leaf() { + found = true + if !bytes.Equal(it.LeafKey(), key[:]) { + t.Fatalf("leaf key mismatch: got %x, want %x", it.LeafKey(), key) + } + if !bytes.Equal(it.LeafBlob(), val[:]) { + t.Fatalf("leaf blob mismatch: got %x, want %x", it.LeafBlob(), val) + } + } + } + if !found { + t.Fatal("expected to find a leaf") + } +} + +// TestIteratorEmptyNodeBacktrack is a regression test for the Empty node +// backtracking bug. Before the fix, encountering an Empty child during +// iteration would terminate the walk prematurely instead of backtracking +// to the parent and continuing with the next sibling. +func TestIteratorEmptyNodeBacktrack(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + {common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + }) + + if _, ok := tr.root.(*InternalNode); !ok { + t.Fatalf("expected InternalNode root, got %T", tr.root) + } + if leaves := countLeaves(t, tr); leaves != 2 { + t.Fatalf("expected 2 leaves, got %d (Empty backtrack bug?)", leaves) + } +} + +// TestIteratorHashedNodeNilData is a regression test for the nil-data guard. +// When nodeResolver encounters a zero-hash HashedNode, it returns (nil, nil). +// The iterator should treat this as Empty and continue rather than panicking. +func TestIteratorHashedNodeNilData(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + {common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + }) + + root, ok := tr.root.(*InternalNode) + if !ok { + t.Fatalf("expected InternalNode root, got %T", tr.root) + } + + // Replace right child with a zero-hash HashedNode. nodeResolver + // short-circuits on common.Hash{} and returns (nil, nil), which + // triggers the nil-data guard in the iterator. + root.right = HashedNode(common.Hash{}) + + // Should not panic; the zero-hash right child should be treated as Empty. + if leaves := countLeaves(t, tr); leaves != 1 { + t.Fatalf("expected 1 leaf (zero-hash right node skipped), got %d", leaves) + } +} + +// TestIteratorManyStems verifies iteration correctness with many stems, +// producing a deep tree structure. +func TestIteratorManyStems(t *testing.T) { + entries := make([][2]common.Hash, 16) + for i := range entries { + var key common.Hash + key[0] = byte(i << 4) + key[31] = 1 + entries[i] = [2]common.Hash{key, oneKey} + } + tr := makeTrie(t, entries) + if leaves := countLeaves(t, tr); leaves != 16 { + t.Fatalf("expected 16 leaves, got %d", leaves) + } +} + +// TestIteratorDeepTree verifies iteration over a trie with stems that share +// a long common prefix, producing many intermediate InternalNodes. +func TestIteratorDeepTree(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0"), oneKey}, + {common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000"), twoKey}, + }) + if leaves := countLeaves(t, tr); leaves != 2 { + t.Fatalf("expected 2 leaves in deep tree, got %d", leaves) + } +} + +// TestIteratorNodeCount verifies the total number of Next(true) calls +// for a known tree structure. +func TestIteratorNodeCount(t *testing.T) { + tr := makeTrie(t, [][2]common.Hash{ + {common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + {common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey}, + }) + + it, err := newBinaryNodeIterator(tr, nil) + if err != nil { + t.Fatal(err) + } + + total := 0 + leaves := 0 + for it.Next(true) { + total++ + if it.Leaf() { + leaves++ + } + } + if leaves != 2 { + t.Fatalf("expected 2 leaves, got %d", leaves) + } + // Root(InternalNode) + leaf1 (from left StemNode) + leaf2 (from right StemNode) = 3 + // StemNodes are not returned as separate steps; the iterator advances + // directly to the first non-nil value within the stem. + if total != 3 { + t.Fatalf("expected 3 total nodes, got %d", total) + } +} diff --git a/trie/bintrie/key_encoding.go b/trie/bintrie/key_encoding.go index 94a22d52d0..c009f1529f 100644 --- a/trie/bintrie/key_encoding.go +++ b/trie/bintrie/key_encoding.go @@ -18,7 +18,6 @@ package bintrie import ( "bytes" - "crypto/sha256" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" @@ -47,13 +46,27 @@ var ( ) func GetBinaryTreeKey(addr common.Address, key []byte) []byte { - hasher := sha256.New() + return getBinaryTreeKey(addr, key, false) +} + +func getBinaryTreeKey(addr common.Address, offset []byte, overflow bool) []byte { + hasher := newSha256() + defer returnSha256(hasher) hasher.Write(zeroHash[:12]) hasher.Write(addr[:]) - hasher.Write(key[:31]) - hasher.Write([]byte{0}) + var buf [32]byte + // key is big endian, hashed value is little endian + for i := range offset[:31] { + buf[i] = offset[30-i] + } + if overflow { + // Overflow detected when adding MAIN_STORAGE_OFFSET, + // reporting it in the shifter 32 byte value. + buf[31] = 1 + } + hasher.Write(buf[:]) k := hasher.Sum(nil) - k[31] = key[31] + k[31] = offset[31] return k } @@ -69,24 +82,29 @@ func GetBinaryTreeKeyCodeHash(addr common.Address) []byte { return GetBinaryTreeKey(addr, k[:]) } -func GetBinaryTreeKeyStorageSlot(address common.Address, key []byte) []byte { - var k [32]byte +func GetBinaryTreeKeyStorageSlot(address common.Address, slotnum []byte) []byte { + var offset [32]byte // Case when the key belongs to the account header - if bytes.Equal(key[:31], zeroHash[:31]) && key[31] < 64 { - k[31] = 64 + key[31] - return GetBinaryTreeKey(address, k[:]) + if bytes.Equal(slotnum[:31], zeroHash[:31]) && slotnum[31] < 64 { + offset[31] = 64 + slotnum[31] + return GetBinaryTreeKey(address, offset[:]) } - // Set the main storage offset - // note that the first 64 bytes of the main offset storage - // are unreachable, which is consistent with the spec and - // what verkle does. - k[0] = 1 // 1 << 248 - copy(k[1:], key[:31]) - k[31] = key[31] - - return GetBinaryTreeKey(address, k[:]) + // Set the main storage offset offset = MAIN_STORAGE_OFFSET + slotnum + // * Note that MAIN_STORAGE_OFFSET is 1 << 248, so the number + // can overflow into a 33rd byte, but since the value is + // shifted by one byte in getBinaryTreeKey, this only takes + // note of the overflow, and the value will be added after + // the shift, in order to avoid allocating an extra byte. + // * Note that the first 64 bytes of the main offset storage + // are unreachable, which is consistent with the spec. + // * Note that `slotnum` is big-endian + overflow := slotnum[0] == 255 + copy(offset[:], slotnum) + offset[0] += 1 // 1 << 248, handle overflow out of band + + return getBinaryTreeKey(address, offset[:], overflow) } func GetBinaryTreeKeyCodeChunk(address common.Address, chunknr *uint256.Int) []byte { diff --git a/trie/bintrie/stem_node.go b/trie/bintrie/stem_node.go index 60856b42ce..3f69261d62 100644 --- a/trie/bintrie/stem_node.go +++ b/trie/bintrie/stem_node.go @@ -18,7 +18,6 @@ package bintrie import ( "bytes" - "crypto/sha256" "errors" "fmt" "slices" @@ -31,6 +30,9 @@ type StemNode struct { Stem []byte // Stem path to get to StemNodeWidth values Values [][]byte // All values, indexed by the last byte of the key. depth int // Depth of the node + + mustRecompute bool // true if the hash needs to be recomputed + hash common.Hash // cached hash when mustRecompute == false } // Get retrieves the value for the given key. @@ -43,7 +45,7 @@ func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int if !bytes.Equal(bt.Stem, key[:StemSize]) { bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1 - n := &InternalNode{depth: bt.depth} + n := &InternalNode{depth: bt.depth, mustRecompute: true} bt.depth++ var child, other *BinaryNode if bitStem == 0 { @@ -68,9 +70,10 @@ func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int var values [StemNodeWidth][]byte values[key[StemSize]] = value *other = &StemNode{ - Stem: slices.Clone(key[:StemSize]), - Values: values[:], - depth: depth + 1, + Stem: slices.Clone(key[:StemSize]), + Values: values[:], + depth: depth + 1, + mustRecompute: true, } } return n, nil @@ -79,6 +82,7 @@ func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int return bt, errors.New("invalid insertion: value length") } bt.Values[key[StemSize]] = value + bt.mustRecompute = true return bt, nil } @@ -89,9 +93,11 @@ func (bt *StemNode) Copy() BinaryNode { values[i] = slices.Clone(v) } return &StemNode{ - Stem: slices.Clone(bt.Stem), - Values: values[:], - depth: bt.depth, + Stem: slices.Clone(bt.Stem), + Values: values[:], + depth: bt.depth, + hash: bt.hash, + mustRecompute: bt.mustRecompute, } } @@ -102,15 +108,22 @@ func (bt *StemNode) GetHeight() int { // Hash returns the hash of the node. func (bt *StemNode) Hash() common.Hash { + if !bt.mustRecompute { + return bt.hash + } + var data [StemNodeWidth]common.Hash + h := newSha256() + defer returnSha256(h) for i, v := range bt.Values { if v != nil { - h := sha256.Sum256(v) - data[i] = common.BytesToHash(h[:]) + h.Reset() + h.Write(v) + h.Sum(data[i][:0]) } } + h.Reset() - h := sha256.New() for level := 1; level <= 8; level++ { for i := range StemNodeWidth / (1 << level) { h.Reset() @@ -130,7 +143,9 @@ func (bt *StemNode) Hash() common.Hash { h.Write(bt.Stem) h.Write([]byte{0}) h.Write(data[0][:]) - return common.BytesToHash(h.Sum(nil)) + bt.hash = common.BytesToHash(h.Sum(nil)) + bt.mustRecompute = false + return bt.hash } // CollectNodes collects all child nodes at a given path, and flushes it @@ -154,7 +169,7 @@ func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolv if !bytes.Equal(bt.Stem, key[:StemSize]) { bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1 - n := &InternalNode{depth: bt.depth} + n := &InternalNode{depth: bt.depth, mustRecompute: true} bt.depth++ var child, other *BinaryNode if bitStem == 0 { @@ -177,9 +192,10 @@ func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolv *other = Empty{} } else { *other = &StemNode{ - Stem: slices.Clone(key[:StemSize]), - Values: values, - depth: n.depth + 1, + Stem: slices.Clone(key[:StemSize]), + Values: values, + depth: n.depth + 1, + mustRecompute: true, } } return n, nil @@ -189,6 +205,7 @@ func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolv for i, v := range values { if v != nil { bt.Values[i] = v + bt.mustRecompute = true } } return bt, nil diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go index d8d6844427..92c1b49e02 100644 --- a/trie/bintrie/stem_node_test.go +++ b/trie/bintrie/stem_node_test.go @@ -220,6 +220,7 @@ func TestStemNodeHash(t *testing.T) { // Changing a value should change the hash node.Values[1] = common.HexToHash("0x0202").Bytes() + node.mustRecompute = true hash3 := node.Hash() if hash1 == hash3 { t.Error("Hash didn't change after modifying values") diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index a509c471b8..6c29239a87 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -143,7 +143,7 @@ func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, err if err != nil { return nil, err } - node, err := DeserializeNode(blob, 0) + node, err := DeserializeNodeWithHash(blob, 0, root) if err != nil { return nil, err } diff --git a/trie/levelstats.go b/trie/levelstats.go index 9168e3fbaf..c73d652146 100644 --- a/trie/levelstats.go +++ b/trie/levelstats.go @@ -36,6 +36,18 @@ func NewLevelStats() *LevelStats { return &LevelStats{} } +// Copy returns a deep copy of the statistics. +func (s *LevelStats) Copy() *LevelStats { + cpy := NewLevelStats() + for i := range s.level { + cpy.level[i].short.Store(s.level[i].short.Load()) + cpy.level[i].full.Store(s.level[i].full.Load()) + cpy.level[i].value.Store(s.level[i].value.Load()) + cpy.level[i].size.Store(s.level[i].size.Load()) + } + return cpy +} + // MaxDepth iterates each level and finds the deepest level with at least one // trie node. func (s *LevelStats) MaxDepth() int { diff --git a/trie/trie_test.go b/trie/trie_test.go index 4761dbf717..7d97676369 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -940,6 +940,7 @@ func (b *spongeBatch) ValueSize() int { return 100 } func (b *spongeBatch) Write() error { return nil } func (b *spongeBatch) Reset() {} func (b *spongeBatch) Replay(w ethdb.KeyValueWriter) error { return nil } +func (b *spongeBatch) Close() {} // TestCommitSequence tests that the trie.Commit operation writes the elements // of the trie in the expected order. diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go index 2e29abcf27..a89b002b59 100644 --- a/triedb/pathdb/buffer.go +++ b/triedb/pathdb/buffer.go @@ -206,6 +206,8 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethd b.flushErr = err return } + batch.Close() + commitBytesMeter.Mark(int64(size)) commitNodesMeter.Mark(int64(nodes)) commitAccountsMeter.Mark(int64(accounts)) diff --git a/triedb/pathdb/config.go b/triedb/pathdb/config.go index e0c23ce9a9..5dffa179b0 100644 --- a/triedb/pathdb/config.go +++ b/triedb/pathdb/config.go @@ -134,10 +134,11 @@ type Config struct { PreloadRateLimit int64 // Testing configurations - SnapshotNoBuild bool // Flag Whether the state generation is disabled - NoAsyncFlush bool // Flag whether the background buffer flushing is disabled - NoAsyncGeneration bool // Flag whether the background generation is disabled - MaxDiffLayers uint64 // Maximum diff layers allowed in the layer tree + SnapshotNoBuild bool // Flag Whether the state generation is disabled + NoAsyncFlush bool // Flag whether the background buffer flushing is disabled + NoAsyncGeneration bool // Flag whether the background generation is disabled + NoHistoryIndexDelay bool // Flag whether the history index delay is disabled + MaxDiffLayers uint64 // Maximum diff layers allowed in the layer tree } // sanitize checks the provided user configurations and changes anything that's diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 5074c04572..410a1b698d 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -215,14 +215,14 @@ func (db *Database) setHistoryIndexer() { if db.stateIndexer != nil { db.stateIndexer.close() } - db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory) + db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory, db.config.NoHistoryIndexDelay) log.Info("Enabled state history indexing") } if db.trienodeFreezer != nil { if db.trienodeIndexer != nil { db.trienodeIndexer.close() } - db.trienodeIndexer = newHistoryIndexer(db.diskdb, db.trienodeFreezer, db.tree.bottom().stateID(), typeTrienodeHistory) + db.trienodeIndexer = newHistoryIndexer(db.diskdb, db.trienodeFreezer, db.tree.bottom().stateID(), typeTrienodeHistory, db.config.NoHistoryIndexDelay) log.Info("Enabled trienode history indexing") } } diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index c85e62d6e1..307088ffce 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -183,6 +183,7 @@ func newTester(t *testing.T, config *testerConfig) *tester { WriteBufferSize: config.writeBufferSize(), NoAsyncFlush: true, JournalDirectory: config.journalDir, + NoHistoryIndexDelay: true, }, config.isVerkle) obj = &tester{ diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index 820c3c03bf..0a9f7091fa 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -412,28 +412,34 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint // Truncate excessive history entries in either the state history or // the trienode history, ensuring both histories remain aligned with // the state. - head, err := states.Ancients() + shead, err := states.Ancients() if err != nil { return nil, nil, err } - if stateID > head { - return nil, nil, fmt.Errorf("gap between state [#%d] and state history [#%d]", stateID, head) + if stateID > shead { // Gap is not permitted in the state history + return nil, nil, fmt.Errorf("gap between state [#%d] and state history [#%d]", stateID, shead) } + truncTo := min(shead, stateID) + if trienodes != nil { - th, err := trienodes.Ancients() + thead, err := trienodes.Ancients() if err != nil { return nil, nil, err } - if stateID > th { - return nil, nil, fmt.Errorf("gap between state [#%d] and trienode history [#%d]", stateID, th) - } - if th != head { - log.Info("Histories are not aligned with each other", "state", head, "trienode", th) - head = min(head, th) + if stateID <= thead { + truncTo = min(truncTo, thead) + } else { + if thead == 0 { + _, err = trienodes.TruncateTail(stateID) + if err != nil { + return nil, nil, err + } + log.Warn("Initialized trienode history") + } else { + return nil, nil, fmt.Errorf("gap between state [#%d] and trienode history [#%d]", stateID, thead) + } } } - head = min(head, stateID) - // Truncate the extra history elements above in freezer in case it's not // aligned with the state. It might happen after an unclean shutdown. truncate := func(store ethdb.AncientStore, typ historyType, nhead uint64) { @@ -448,7 +454,7 @@ func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint log.Warn("Truncated extra histories", "typ", typ, "number", pruned) } } - truncate(states, typeStateHistory, head) - truncate(trienodes, typeTrienodeHistory, head) + truncate(states, typeStateHistory, truncTo) + truncate(trienodes, typeTrienodeHistory, truncTo) return states, trienodes, nil } diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index 487e510bd2..4f8b2205b2 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -42,6 +42,8 @@ const ( stateHistoryIndexVersion = stateHistoryIndexV0 // the current state index version trienodeHistoryIndexV0 = uint8(0) // initial version of trienode index structure trienodeHistoryIndexVersion = trienodeHistoryIndexV0 // the current trienode index version + + indexerProcessBatchInSync = 100000 // threshold for history batch indexing when node is in sync stage. ) // indexVersion returns the latest index version for the given history type. @@ -350,7 +352,8 @@ type interruptSignal struct { // If a state history is removed due to a rollback, the associated indexes should // be unmarked accordingly. type indexIniter struct { - disk ethdb.KeyValueStore + state *initerState + disk ethdb.Database freezer ethdb.AncientStore interrupt chan *interruptSignal done chan struct{} @@ -365,8 +368,9 @@ type indexIniter struct { wg sync.WaitGroup } -func newIndexIniter(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, typ historyType, lastID uint64) *indexIniter { +func newIndexIniter(disk ethdb.Database, freezer ethdb.AncientStore, typ historyType, lastID uint64, noWait bool) *indexIniter { initer := &indexIniter{ + state: newIniterState(disk, noWait), disk: disk, freezer: freezer, interrupt: make(chan *interruptSignal), @@ -386,12 +390,7 @@ func newIndexIniter(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, typ hi // Launch background indexer initer.wg.Add(1) - if recover { - log.Info("History indexer is recovering", "history", lastID, "indexed", metadata.Last) - go initer.recover(lastID) - } else { - go initer.run(lastID) - } + go initer.run(recover) return initer } @@ -401,6 +400,7 @@ func (i *indexIniter) close() { return default: close(i.closed) + i.state.close() i.wg.Wait() } } @@ -432,85 +432,109 @@ func (i *indexIniter) remain() uint64 { } } -func (i *indexIniter) run(lastID uint64) { +func (i *indexIniter) run(recover bool) { defer i.wg.Done() // Launch background indexing thread var ( - done = make(chan struct{}) - interrupt = new(atomic.Int32) + done chan struct{} + interrupt *atomic.Int32 - // checkDone indicates whether all requested state histories - // have been fully indexed. + // checkDone reports whether indexing has completed for all histories. checkDone = func() bool { metadata := loadIndexMetadata(i.disk, i.typ) - return metadata != nil && metadata.Last == lastID + return metadata != nil && metadata.Last == i.last.Load() + } + // canExit reports whether the initial indexing phase has completed. + canExit = func() bool { + return !i.state.is(stateSyncing) && checkDone() } + heartBeat = time.NewTimer(0) ) - go i.index(done, interrupt, lastID) + defer heartBeat.Stop() + if recover { + if aborted := i.recover(); aborted { + return + } + } for { select { case signal := <-i.interrupt: - // The indexing limit can only be extended or shortened continuously. newLastID := signal.newLastID - if newLastID != lastID+1 && newLastID != lastID-1 { - signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", lastID, newLastID) + oldLastID := i.last.Load() + + // The indexing limit can only be extended or shortened continuously. + if newLastID != oldLastID+1 && newLastID != oldLastID-1 { + signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", oldLastID, newLastID) continue } i.last.Store(newLastID) // update indexing range // The index limit is extended by one, update the limit without // interrupting the current background process. - if newLastID == lastID+1 { - lastID = newLastID + if newLastID == oldLastID+1 { signal.result <- nil - i.log.Debug("Extended history range", "last", lastID) + i.log.Debug("Extended history range", "last", newLastID) continue } - // The index limit is shortened by one, interrupt the current background - // process and relaunch with new target. - interrupt.Store(1) - <-done - + // The index limit is shortened, interrupt the current background + // process if it's active and update the target. + if done != nil { + interrupt.Store(1) + <-done + done, interrupt = nil, nil + } // If all state histories, including the one to be reverted, have // been fully indexed, unindex it here and shut down the initializer. if checkDone() { - i.log.Info("Truncate the extra history", "id", lastID) - if err := unindexSingle(lastID, i.disk, i.freezer, i.typ); err != nil { + i.log.Info("Truncate the extra history", "id", oldLastID) + if err := unindexSingle(oldLastID, i.disk, i.freezer, i.typ); err != nil { signal.result <- err return } close(i.done) signal.result <- nil - i.log.Info("Histories have been fully indexed", "last", lastID-1) + i.log.Info("Histories have been fully indexed", "last", i.last.Load()) return } - // Adjust the indexing target and relaunch the process - lastID = newLastID + // Adjust the indexing target signal.result <- nil - - done, interrupt = make(chan struct{}), new(atomic.Int32) - go i.index(done, interrupt, lastID) - i.log.Debug("Shortened history range", "last", lastID) + i.log.Debug("Shortened history range", "last", newLastID) case <-done: - if checkDone() { + done, interrupt = nil, nil + + if canExit() { close(i.done) - i.log.Info("Histories have been fully indexed", "last", lastID) return } - // Relaunch the background runner if some tasks are left - done, interrupt = make(chan struct{}), new(atomic.Int32) - go i.index(done, interrupt, lastID) - case <-i.closed: - interrupt.Store(1) - i.log.Info("Waiting background history index initer to exit") - <-done + case <-heartBeat.C: + heartBeat.Reset(time.Second * 15) - if checkDone() { + // Short circuit if the indexer is still busy + if done != nil { + continue + } + if canExit() { close(i.done) + return + } + // The local chain is still in the syncing phase. Only start the indexing + // when a sufficient amount of histories has accumulated. Batch indexing + // is more efficient than processing items individually. + if i.state.is(stateSyncing) && i.last.Load()-i.indexed.Load() < indexerProcessBatchInSync { + continue + } + done, interrupt = make(chan struct{}), new(atomic.Int32) + go i.index(done, interrupt, i.last.Load()) + + case <-i.closed: + if done != nil { + interrupt.Store(1) + i.log.Info("Waiting background history index initer to exit") + <-done } return } @@ -572,7 +596,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID } return } - i.log.Info("Start history indexing", "beginID", beginID, "lastID", lastID) + i.log.Debug("Start history indexing", "beginID", beginID, "lastID", lastID) var ( current = beginID @@ -619,7 +643,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID done = current - beginID ) eta := common.CalculateETA(done, left, time.Since(start)) - i.log.Info("Indexing history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta)) + i.log.Debug("Indexing history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta)) } } i.indexed.Store(current - 1) // update indexing progress @@ -630,7 +654,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID if err := batch.finish(true); err != nil { i.log.Error("Failed to flush index", "err", err) } - log.Info("State indexing interrupted") + log.Debug("State indexing interrupted") return } } @@ -638,7 +662,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID if err := batch.finish(true); err != nil { i.log.Error("Failed to flush index", "err", err) } - i.log.Info("Indexed history", "from", beginID, "to", lastID, "elapsed", common.PrettyDuration(time.Since(start))) + i.log.Debug("Indexed history", "from", beginID, "to", lastID, "elapsed", common.PrettyDuration(time.Since(start))) } // recover handles unclean shutdown recovery. After an unclean shutdown, any @@ -651,35 +675,35 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID // by chain recovery, under the assumption that the recovered histories will be // identical to the lost ones. Fork-awareness should be added in the future to // correctly handle histories affected by reorgs. -func (i *indexIniter) recover(lastID uint64) { - defer i.wg.Done() +func (i *indexIniter) recover() bool { + log.Info("History indexer is recovering", "last", i.last.Load(), "indexed", i.indexed.Load()) for { select { case signal := <-i.interrupt: newLastID := signal.newLastID - if newLastID != lastID+1 && newLastID != lastID-1 { - signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", lastID, newLastID) + oldLastID := i.last.Load() + + // The indexing limit can only be extended or shortened continuously. + if newLastID != oldLastID+1 && newLastID != oldLastID-1 { + signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", oldLastID, newLastID) continue } - // Update the last indexed flag - lastID = newLastID signal.result <- nil i.last.Store(newLastID) - i.log.Debug("Updated history index flag", "last", lastID) + i.log.Debug("Updated history index flag", "last", newLastID) // Terminate the recovery routine once the histories are fully aligned // with the index data, indicating that index initialization is complete. metadata := loadIndexMetadata(i.disk, i.typ) - if metadata != nil && metadata.Last == lastID { - close(i.done) - i.log.Info("History indexer is recovered", "last", lastID) - return + if metadata != nil && metadata.Last == newLastID { + i.log.Info("History indexer is recovered", "last", newLastID) + return false } case <-i.closed: - return + return true } } } @@ -747,10 +771,10 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) { // newHistoryIndexer constructs the history indexer and launches the background // initer to complete the indexing of any remaining state histories. -func newHistoryIndexer(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, lastHistoryID uint64, typ historyType) *historyIndexer { +func newHistoryIndexer(disk ethdb.Database, freezer ethdb.AncientStore, lastHistoryID uint64, typ historyType, noWait bool) *historyIndexer { checkVersion(disk, typ) return &historyIndexer{ - initer: newIndexIniter(disk, freezer, typ, lastHistoryID), + initer: newIndexIniter(disk, freezer, typ, lastHistoryID, noWait), typ: typ, disk: disk, freezer: freezer, diff --git a/triedb/pathdb/history_indexer_state.go b/triedb/pathdb/history_indexer_state.go new file mode 100644 index 0000000000..2746083297 --- /dev/null +++ b/triedb/pathdb/history_indexer_state.go @@ -0,0 +1,183 @@ +// 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 pathdb + +import ( + "bytes" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +// state represents the syncing status of the node. +type state int + +const ( + // stateSynced indicates that the local chain head is sufficiently close to the + // network chain head, and the majority of the data has been fully synchronized. + stateSynced state = iota + + // stateSyncing indicates that the sync process is still in progress. Local node + // is actively catching up with the network chain head. + stateSyncing + + // stateStalled indicates that sync progress has stopped for a while + // with no progress. This may be caused by network instability (e.g., no peers), + // manual operation such as syncing the local chain to a specific block. + stateStalled +) + +const ( + // syncStateTimeWindow defines the maximum allowed lag behind the network + // chain head. + // + // If the local chain head falls within this threshold, the node is considered + // close to the tip and will be marked as stateSynced. + syncStateTimeWindow = 6 * time.Hour + + // syncStalledTimeout defines the maximum duration during which no sync + // progress is observed. If this timeout is exceeded, the node's status + // will be considered stalled. + syncStalledTimeout = 5 * time.Minute +) + +type initerState struct { + state state + stateLock sync.Mutex + disk ethdb.Database + term chan struct{} +} + +func newIniterState(disk ethdb.Database, noWait bool) *initerState { + s := &initerState{ + state: stateSyncing, + disk: disk, + term: make(chan struct{}), + } + go s.update(noWait) + return s +} + +func (s *initerState) get() state { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + return s.state +} + +func (s *initerState) is(state state) bool { + return s.get() == state +} + +func (s *initerState) set(state state) { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + s.state = state +} + +func (s *initerState) update(noWait bool) { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + headBlock := s.readLastBlock() + if headBlock != nil && time.Since(time.Unix(int64(headBlock.Time), 0)) < syncStateTimeWindow { + s.set(stateSynced) + log.Info("Marked indexing initer as synced") + } else if noWait { + s.set(stateSynced) + log.Info("Marked indexing initer as synced forcibly") + } else { + s.set(stateSyncing) + } + + var ( + hhash = rawdb.ReadHeadHeaderHash(s.disk) + fhash = rawdb.ReadHeadFastBlockHash(s.disk) + bhash = rawdb.ReadHeadBlockHash(s.disk) + skeleton = rawdb.ReadSkeletonSyncStatus(s.disk) + lastProgress = time.Now() + ) + for { + select { + case <-ticker.C: + state := s.get() + if state == stateSynced || state == stateStalled { + continue + } + headBlock := s.readLastBlock() + if headBlock == nil { + continue + } + // State machine: stateSyncing => stateSynced + if time.Since(time.Unix(int64(headBlock.Time), 0)) < syncStateTimeWindow { + s.set(stateSynced) + log.Info("Marked indexing initer as synced") + continue + } + // State machine: stateSyncing => stateStalled + newhhash := rawdb.ReadHeadHeaderHash(s.disk) + newfhash := rawdb.ReadHeadFastBlockHash(s.disk) + newbhash := rawdb.ReadHeadBlockHash(s.disk) + newskeleton := rawdb.ReadSkeletonSyncStatus(s.disk) + hasProgress := newhhash.Cmp(hhash) != 0 || newfhash.Cmp(fhash) != 0 || newbhash.Cmp(bhash) != 0 || !bytes.Equal(newskeleton, skeleton) + + if !hasProgress && time.Since(lastProgress) > syncStalledTimeout { + s.set(stateStalled) + log.Info("Marked indexing initer as stalled") + continue + } + if hasProgress { + hhash = newhhash + fhash = newfhash + bhash = newbhash + skeleton = newskeleton + lastProgress = time.Now() + } + + case <-s.term: + return + } + } +} + +func (s *initerState) close() { + select { + case <-s.term: + default: + close(s.term) + } + return +} + +// readLastBlock returns the local chain head. +func (s *initerState) readLastBlock() *types.Header { + hash := rawdb.ReadHeadBlockHash(s.disk) + if hash == (common.Hash{}) { + return nil + } + number, exists := rawdb.ReadHeaderNumber(s.disk, hash) + if !exists { + return nil + } + return rawdb.ReadHeader(s.disk, hash, number) +} diff --git a/triedb/pathdb/history_indexer_test.go b/triedb/pathdb/history_indexer_test.go index f333d18d8b..8bb1db42da 100644 --- a/triedb/pathdb/history_indexer_test.go +++ b/triedb/pathdb/history_indexer_test.go @@ -27,7 +27,7 @@ import ( // deadlock when the indexer is active. This specifically targets the case where // signal.result must be sent to unblock the caller. func TestHistoryIndexerShortenDeadlock(t *testing.T) { - //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + // log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true))) db := rawdb.NewMemoryDatabase() freezer, _ := rawdb.NewStateFreezer(t.TempDir(), false, false) defer freezer.Close() @@ -38,7 +38,7 @@ func TestHistoryIndexerShortenDeadlock(t *testing.T) { rawdb.WriteStateHistory(freezer, uint64(i+1), h.meta.encode(), accountIndex, storageIndex, accountData, storageData) } // As a workaround, assign a future block to keep the initer running indefinitely - indexer := newHistoryIndexer(db, freezer, 200, typeStateHistory) + indexer := newHistoryIndexer(db, freezer, 200, typeStateHistory, true) defer indexer.close() done := make(chan error, 1) diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index e0d86bdeaa..05e7c96294 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -380,7 +380,7 @@ func (db *Database) HistoricNodeReader(root common.Hash) (*HistoricalNodeReader, // are not accessible. meta, err := readTrienodeMetadata(db.trienodeFreezer, *id+1) if err != nil { - return nil, err // e.g., the referred trienode history has been pruned + return nil, fmt.Errorf("state %#x is not available", root) // e.g., the referred trienode history has been pruned } if meta.parent != root { return nil, fmt.Errorf("state %#x is not canonincal", root)