mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
trie, cmd/geth: add archive verify command, Walk(), and archiver improvements
Add Trie.Walk() for exhaustive traversal that resolves expired nodes with hash verification. Add `archive verify` subcommand that walks the full state (account + storage tries) to validate all archived data can be correctly resurrected. Delete both the journal KV entry and file after archiving to force geth to restart with a bare disk layer, rewinding the chain head to the persistent disk state and re-executing blocks. Also adds markSubtreeDirty() to resolveExpiredNodeData() so that all nodes in a resolved expired subtree are captured in the NodeSet during commit — preventing them from being lost between diff layers and the raw DB.
This commit is contained in:
parent
8d2125e4fd
commit
5119945e25
15 changed files with 1032 additions and 282 deletions
|
|
@ -17,8 +17,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
|
@ -26,10 +28,14 @@ import (
|
|||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"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/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
"github.com/ethereum/go-ethereum/trie/archive"
|
||||
"github.com/ethereum/go-ethereum/triedb/database"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
|
|
@ -51,14 +57,73 @@ var (
|
|||
}
|
||||
|
||||
// Commands
|
||||
archiveCheckNodeFlag = &cli.StringFlag{
|
||||
Name: "owner",
|
||||
Usage: "Owner hash (hex) for the trie node to check",
|
||||
}
|
||||
archiveCheckPathFlag = &cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "Path (hex nibbles) of the trie node to check",
|
||||
}
|
||||
|
||||
archiveCommand = &cli.Command{
|
||||
Name: "archive",
|
||||
Usage: "Archive state trie nodes to reduce database size",
|
||||
Subcommands: []*cli.Command{
|
||||
archiveGenerateCmd,
|
||||
archiveVerifyCmd,
|
||||
archiveDeleteJournalCmd,
|
||||
archiveCheckNodeCmd,
|
||||
},
|
||||
}
|
||||
|
||||
archiveCheckNodeCmd = &cli.Command{
|
||||
Name: "check-node",
|
||||
Usage: "Check if a specific trie node exists in the raw DB",
|
||||
Action: archiveCheckNode,
|
||||
Flags: slices.Concat([]cli.Flag{
|
||||
archiveCheckNodeFlag,
|
||||
archiveCheckPathFlag,
|
||||
}, utils.NetworkFlags, utils.DatabaseFlags),
|
||||
}
|
||||
|
||||
archiveDeleteJournalCmd = &cli.Command{
|
||||
Name: "delete-journal",
|
||||
Usage: "Delete the pathdb journal to force a clean restart",
|
||||
Action: archiveDeleteJournal,
|
||||
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
|
||||
Description: `
|
||||
Deletes the pathdb journal (TrieJournal key and merkle.journal file) from the
|
||||
database. This forces geth to restart with a bare disk layer, discarding any
|
||||
in-memory diff layers that may be inconsistent with archived state.
|
||||
|
||||
Use this after running 'archive generate' if geth was started in between and
|
||||
recreated the journal.
|
||||
|
||||
Examples:
|
||||
geth archive delete-journal --datadir /path/to/datadir
|
||||
geth archive delete-journal --hoodi
|
||||
`,
|
||||
}
|
||||
|
||||
archiveVerifyCmd = &cli.Command{
|
||||
Name: "verify",
|
||||
Usage: "Verify all archived nodes can be correctly resurrected",
|
||||
Action: archiveVerify,
|
||||
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
|
||||
Description: `
|
||||
Walks the entire state trie, resolving every expired node from the archive
|
||||
file and verifying that the reconstructed subtree hash matches the original.
|
||||
Also walks all storage tries referenced by accounts.
|
||||
|
||||
The database is opened read-only. No modifications are made.
|
||||
|
||||
Examples:
|
||||
geth archive verify --datadir /path/to/datadir
|
||||
geth archive verify --hoodi
|
||||
`,
|
||||
}
|
||||
|
||||
archiveGenerateCmd = &cli.Command{
|
||||
Name: "generate",
|
||||
Usage: "Generate archive files from height-3 subtrees",
|
||||
|
|
@ -77,22 +142,56 @@ that references the archive file offset and size.
|
|||
Height is measured from leaves: leaves=0, parents=1, etc. A height-3 node
|
||||
has leaves at most 3 levels below it.
|
||||
|
||||
The archiver reads trie nodes directly from the persistent database layer,
|
||||
bypassing any in-memory diff layers. This ensures consistency between the
|
||||
data it reads and the data it modifies.
|
||||
|
||||
Examples:
|
||||
# Archive from head state
|
||||
# Archive from the persistent disk state
|
||||
geth archive generate --datadir /path/to/datadir
|
||||
|
||||
# Dry run to see what would be archived
|
||||
geth archive generate --dry-run --datadir /path/to/datadir
|
||||
|
||||
# Archive from a specific state root
|
||||
geth archive generate 0x1234...abcd --datadir /path/to/datadir
|
||||
|
||||
# Custom output and compaction interval
|
||||
geth archive generate --output /path/to/archive --compaction-interval 500
|
||||
`,
|
||||
}
|
||||
)
|
||||
|
||||
// rawDBNodeReader implements database.NodeReader by reading trie nodes directly
|
||||
// from the raw key-value database, bypassing pathdb's in-memory diff layers.
|
||||
// This ensures the archiver sees the same trie state it modifies.
|
||||
type rawDBNodeReader struct {
|
||||
db ethdb.KeyValueReader
|
||||
}
|
||||
|
||||
func (r *rawDBNodeReader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) {
|
||||
var blob []byte
|
||||
if owner == (common.Hash{}) {
|
||||
blob = rawdb.ReadAccountTrieNode(r.db, path)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageTrieNode(r.db, owner, path)
|
||||
}
|
||||
// Skip hash verification: the raw DB may contain expiredNode markers
|
||||
// (blob[0] == 0x00) which have different hashes than the original nodes.
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
// rawDBNodeDatabase implements database.NodeDatabase using direct raw DB reads.
|
||||
type rawDBNodeDatabase struct {
|
||||
db ethdb.KeyValueReader
|
||||
root common.Hash
|
||||
}
|
||||
|
||||
func (d *rawDBNodeDatabase) NodeReader(stateRoot common.Hash) (database.NodeReader, error) {
|
||||
// Only allow reading the persistent disk root state
|
||||
if stateRoot != d.root {
|
||||
return nil, fmt.Errorf("raw DB reader only supports disk root %x, got %x", d.root, stateRoot)
|
||||
}
|
||||
return &rawDBNodeReader{db: d.db}, nil
|
||||
}
|
||||
|
||||
func archiveGenerate(ctx *cli.Context) error {
|
||||
// 1. Setup node and databases
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
|
|
@ -109,27 +208,25 @@ func archiveGenerate(ctx *cli.Context) error {
|
|||
return fmt.Errorf("archive generation requires path-based state scheme, got: %s", scheme)
|
||||
}
|
||||
|
||||
triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, false, false)
|
||||
defer triedb.Close()
|
||||
|
||||
// 2. Determine state root
|
||||
var root common.Hash
|
||||
if ctx.NArg() > 0 {
|
||||
root = common.HexToHash(ctx.Args().First())
|
||||
log.Info("Using specified state root", "root", root)
|
||||
} else {
|
||||
headBlock := rawdb.ReadHeadBlock(chaindb)
|
||||
if headBlock == nil {
|
||||
return errors.New("no head block found - specify a state root or sync the chain first")
|
||||
}
|
||||
root = headBlock.Root()
|
||||
log.Info("Using head block state", "number", headBlock.NumberU64(), "root", root)
|
||||
}
|
||||
|
||||
// Verify the state exists
|
||||
if !rawdb.HasAccountTrieNode(chaindb, nil) {
|
||||
// 2. Determine the persistent disk state root.
|
||||
//
|
||||
// The archiver reads and writes directly to the raw key-value database,
|
||||
// bypassing pathdb's in-memory diff layers. This avoids the inconsistency
|
||||
// where diff layers shadow expiredNode markers written to disk.
|
||||
//
|
||||
// The disk root is computed by hashing the account trie root node stored
|
||||
// in the raw database. This root corresponds to the last state that was
|
||||
// fully persisted (i.e., PersistentStateID), which matches the canonical
|
||||
// chain head.
|
||||
rootBlob := rawdb.ReadAccountTrieNode(chaindb, nil)
|
||||
if len(rootBlob) == 0 {
|
||||
return errors.New("state trie not found in database")
|
||||
}
|
||||
root := crypto.Keccak256Hash(rootBlob)
|
||||
log.Info("Using persistent disk state root", "root", root)
|
||||
|
||||
// Create a raw DB node reader that bypasses pathdb layers
|
||||
nodeDB := &rawDBNodeDatabase{db: chaindb, root: root}
|
||||
|
||||
// 3. Open archive writer (unless dry-run)
|
||||
var writer *archive.ArchiveWriter
|
||||
|
|
@ -153,7 +250,7 @@ func archiveGenerate(ctx *cli.Context) error {
|
|||
// 4. Create and run archiver
|
||||
archiver := trie.NewArchiver(
|
||||
chaindb,
|
||||
triedb,
|
||||
nodeDB,
|
||||
writer,
|
||||
ctx.Uint64(archiveCompactionIntervalFlag.Name),
|
||||
dryRun,
|
||||
|
|
@ -174,6 +271,27 @@ func archiveGenerate(ctx *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
// Delete the pathdb journal. The archiver modified the raw DB
|
||||
// underneath the diff layers, so the journal's buffered state is
|
||||
// inconsistent. Deleting forces geth to restart with a bare disk
|
||||
// layer and rewind the chain head to the disk state.
|
||||
if err := chaindb.Delete([]byte("TrieJournal")); err != nil {
|
||||
log.Warn("Failed to delete pathdb journal key", "err", err)
|
||||
}
|
||||
log.Info("Deleted pathdb journal to force clean restart")
|
||||
|
||||
// Delete journal file(s) - check both legacy and current locations
|
||||
for _, dir := range []string{"triedb", ""} {
|
||||
for _, name := range []string{"merkle.journal", "verkle.journal"} {
|
||||
journalFile := filepath.Join(stack.ResolvePath(dir), name)
|
||||
if err := os.Remove(journalFile); err == nil {
|
||||
log.Info("Deleted journal file", "path", journalFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Print summary
|
||||
var archiveSize uint64
|
||||
if writer != nil {
|
||||
|
|
@ -194,6 +312,258 @@ func archiveGenerate(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func archiveVerify(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
defer stack.Close()
|
||||
|
||||
// Open database read-only
|
||||
chaindb := utils.MakeChainDatabase(ctx, stack, true)
|
||||
defer chaindb.Close()
|
||||
|
||||
scheme := cycleCheckScheme(ctx, chaindb)
|
||||
if scheme != rawdb.PathScheme {
|
||||
return fmt.Errorf("archive verify requires path-based state scheme, got: %s", scheme)
|
||||
}
|
||||
|
||||
// Set archive data dir so ArchivedNodeResolver can find the file
|
||||
// ResolvePath("") returns the node's data directory (e.g. .ethereum/hoodi/geth),
|
||||
// but ArchivedNodeResolver expects the instance directory (.ethereum/hoodi)
|
||||
// since it appends "geth/nodearchive" itself.
|
||||
archive.ArchiveDataDir = filepath.Dir(stack.ResolvePath(""))
|
||||
|
||||
// Compute disk root
|
||||
rootBlob := rawdb.ReadAccountTrieNode(chaindb, nil)
|
||||
if len(rootBlob) == 0 {
|
||||
return errors.New("state trie not found in database")
|
||||
}
|
||||
root := crypto.Keccak256Hash(rootBlob)
|
||||
log.Info("Verifying archived nodes", "root", root)
|
||||
|
||||
nodeDB := &rawDBNodeDatabase{db: chaindb, root: root}
|
||||
|
||||
// Open account trie
|
||||
accountTrie, err := trie.New(trie.StateTrieID(root), nodeDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open account trie: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
totalAccounts int
|
||||
totalStorageTries int
|
||||
totalLeaves int
|
||||
totalExpired int
|
||||
totalErrors int
|
||||
start = time.Now()
|
||||
lastLog = time.Now()
|
||||
)
|
||||
|
||||
// Walk the account trie — this resolves all expired nodes and verifies hashes
|
||||
accountStats, err := accountTrie.Walk(func(path []byte, value []byte) error {
|
||||
totalAccounts++
|
||||
if time.Since(lastLog) > 30*time.Second {
|
||||
log.Info("Verification progress",
|
||||
"accounts", totalAccounts,
|
||||
"storageTries", totalStorageTries,
|
||||
"leaves", totalLeaves,
|
||||
"expired", totalExpired,
|
||||
"errors", totalErrors)
|
||||
lastLog = time.Now()
|
||||
}
|
||||
|
||||
// Decode account to check for storage trie
|
||||
var acc types.StateAccount
|
||||
if err := rlp.DecodeBytes(value, &acc); err != nil {
|
||||
log.Warn("Failed to decode account", "err", err)
|
||||
totalErrors++
|
||||
return nil // continue walking
|
||||
}
|
||||
if acc.Root == types.EmptyRootHash {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open and walk storage trie.
|
||||
// path is hex-nibble encoded (with a 16 terminator from the trie key),
|
||||
// so convert nibble pairs back to the 32-byte account hash.
|
||||
nibbles := path
|
||||
if len(nibbles) > 0 && nibbles[len(nibbles)-1] == 16 {
|
||||
nibbles = nibbles[:len(nibbles)-1]
|
||||
}
|
||||
keyBytes := make([]byte, len(nibbles)/2)
|
||||
for i := 0; i < len(nibbles); i += 2 {
|
||||
keyBytes[i/2] = nibbles[i]<<4 | nibbles[i+1]
|
||||
}
|
||||
accountHash := common.BytesToHash(keyBytes)
|
||||
storageID := trie.StorageTrieID(root, accountHash, acc.Root)
|
||||
storageTrie, err := trie.New(storageID, nodeDB)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open storage trie", "account", accountHash, "err", err)
|
||||
totalErrors++
|
||||
return nil
|
||||
}
|
||||
|
||||
storageStats, err := storageTrie.Walk(func(spath []byte, svalue []byte) error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Storage trie walk failed", "account", accountHash, "err", err)
|
||||
totalErrors++
|
||||
return nil
|
||||
}
|
||||
totalStorageTries++
|
||||
totalLeaves += storageStats.Leaves
|
||||
totalExpired += storageStats.ExpiredResolved
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("account trie walk failed: %w", err)
|
||||
}
|
||||
|
||||
totalLeaves += accountStats.Leaves
|
||||
totalExpired += accountStats.ExpiredResolved
|
||||
|
||||
log.Info("Archive verification complete",
|
||||
"accounts", totalAccounts,
|
||||
"storageTries", totalStorageTries,
|
||||
"totalLeaves", totalLeaves,
|
||||
"expiredResolved", totalExpired,
|
||||
"errors", totalErrors,
|
||||
"elapsed", common.PrettyDuration(time.Since(start)))
|
||||
|
||||
if totalErrors > 0 {
|
||||
return fmt.Errorf("verification completed with %d errors", totalErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func archiveDeleteJournal(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
defer stack.Close()
|
||||
|
||||
chaindb := utils.MakeChainDatabase(ctx, stack, false)
|
||||
defer chaindb.Close()
|
||||
|
||||
// Delete the pathdb journal KV key
|
||||
if err := chaindb.Delete([]byte("TrieJournal")); err != nil {
|
||||
log.Warn("Failed to delete pathdb journal key", "err", err)
|
||||
} else {
|
||||
log.Info("Deleted pathdb journal key (TrieJournal)")
|
||||
}
|
||||
|
||||
// Delete the journal file(s) - check both legacy and current locations
|
||||
for _, dir := range []string{"triedb", ""} {
|
||||
for _, name := range []string{"merkle.journal", "verkle.journal"} {
|
||||
journalFile := filepath.Join(stack.ResolvePath(dir), name)
|
||||
if err := os.Remove(journalFile); err == nil {
|
||||
log.Info("Deleted journal file", "path", journalFile)
|
||||
} else if !os.IsNotExist(err) {
|
||||
log.Warn("Failed to delete journal file", "path", journalFile, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func archiveCheckNode(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
defer stack.Close()
|
||||
|
||||
chaindb := utils.MakeChainDatabase(ctx, stack, true)
|
||||
defer chaindb.Close()
|
||||
|
||||
ownerHex := ctx.String(archiveCheckNodeFlag.Name)
|
||||
pathHex := ctx.String(archiveCheckPathFlag.Name)
|
||||
|
||||
if ownerHex == "" {
|
||||
return errors.New("--owner flag is required")
|
||||
}
|
||||
|
||||
owner := common.HexToHash(ownerHex)
|
||||
|
||||
// Parse path: hex nibbles like "08" → []byte{0, 8}
|
||||
var path []byte
|
||||
for _, c := range pathHex {
|
||||
var nibble byte
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
nibble = byte(c - '0')
|
||||
case c >= 'a' && c <= 'f':
|
||||
nibble = byte(c-'a') + 10
|
||||
case c >= 'A' && c <= 'F':
|
||||
nibble = byte(c-'A') + 10
|
||||
default:
|
||||
return fmt.Errorf("invalid hex char in path: %c", c)
|
||||
}
|
||||
path = append(path, nibble)
|
||||
}
|
||||
|
||||
log.Info("Checking node in raw DB", "owner", owner, "path", fmt.Sprintf("%x", path))
|
||||
|
||||
// Read the node directly from the raw DB
|
||||
isAccount := owner == (common.Hash{})
|
||||
|
||||
// Check the target path and all prefixes up to root
|
||||
for i := len(path); i >= 0; i-- {
|
||||
subpath := path[:i]
|
||||
var blob []byte
|
||||
if isAccount {
|
||||
blob = rawdb.ReadAccountTrieNode(chaindb, subpath)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageTrieNode(chaindb, owner, subpath)
|
||||
}
|
||||
|
||||
status := "MISSING"
|
||||
details := ""
|
||||
if len(blob) > 0 {
|
||||
if blob[0] == 0x00 {
|
||||
status = "EXPIRED"
|
||||
if len(blob) == 17 {
|
||||
offset := binary.BigEndian.Uint64(blob[1:9])
|
||||
size := binary.BigEndian.Uint64(blob[9:17])
|
||||
details = fmt.Sprintf("offset=%d size=%d", offset, size)
|
||||
}
|
||||
} else {
|
||||
status = fmt.Sprintf("PRESENT (%d bytes, first=0x%02x)", len(blob), blob[0])
|
||||
}
|
||||
}
|
||||
label := "prefix"
|
||||
if i == len(path) {
|
||||
label = "TARGET"
|
||||
}
|
||||
if i == 0 {
|
||||
label = "ROOT"
|
||||
}
|
||||
log.Info("Node check",
|
||||
"label", label,
|
||||
"path", fmt.Sprintf("%x", subpath),
|
||||
"pathLen", i,
|
||||
"status", status,
|
||||
"details", details)
|
||||
}
|
||||
|
||||
// Also check a few child paths to see what's below the target
|
||||
for nibble := byte(0); nibble < 16; nibble++ {
|
||||
childPath := append(append([]byte{}, path...), nibble)
|
||||
var blob []byte
|
||||
if isAccount {
|
||||
blob = rawdb.ReadAccountTrieNode(chaindb, childPath)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageTrieNode(chaindb, owner, childPath)
|
||||
}
|
||||
if len(blob) > 0 {
|
||||
status := fmt.Sprintf("PRESENT (%d bytes, first=0x%02x)", len(blob), blob[0])
|
||||
if blob[0] == 0x00 && len(blob) == 17 {
|
||||
offset := binary.BigEndian.Uint64(blob[1:9])
|
||||
size := binary.BigEndian.Uint64(blob[9:17])
|
||||
status = fmt.Sprintf("EXPIRED offset=%d size=%d", offset, size)
|
||||
}
|
||||
log.Info("Child node", "path", fmt.Sprintf("%x", childPath), "status", status)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cycleCheckScheme returns the state scheme for the database.
|
||||
// It's a helper to check what scheme is in use.
|
||||
func cycleCheckScheme(ctx *cli.Context, db ethdb.Database) string {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/trie/archive"
|
||||
"github.com/gofrs/flock"
|
||||
)
|
||||
|
||||
|
|
@ -85,6 +86,7 @@ func New(conf *Config) (*Node, error) {
|
|||
return nil, err
|
||||
}
|
||||
conf.DataDir = absdatadir
|
||||
archive.ArchiveDataDir = absdatadir
|
||||
}
|
||||
if conf.Logger == nil {
|
||||
conf.Logger = log.New()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
|
@ -45,13 +46,16 @@ type Record struct {
|
|||
Value []byte
|
||||
}
|
||||
|
||||
// ArchiveDataDir is the data directory where the archive file is stored.
|
||||
var ArchiveDataDir string
|
||||
|
||||
// ArchivedNodeResolver takes a buffer containing the archive data
|
||||
// held by an expiring node (an offset and a size) and returns a
|
||||
// list of records, which is a list of serialized leaf nodes. The
|
||||
// caller knows the context (MPT, binary trie) and is responsible
|
||||
// for decoding the nodes.
|
||||
func ArchivedNodeResolver(offset, size uint64) ([]*Record, error) {
|
||||
file, err := os.Open("nodearchive")
|
||||
file, err := os.Open(filepath.Join(ArchiveDataDir, "geth", "nodearchive"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening archive file: %w", err)
|
||||
}
|
||||
|
|
|
|||
140
trie/archiver.go
140
trie/archiver.go
|
|
@ -19,6 +19,7 @@ package trie
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
|
|
@ -39,6 +40,7 @@ type subtreeInfo struct {
|
|||
height int // Height of subtree (from leaves)
|
||||
leaves []*archive.Record // All leaf records (relative path + encoded node)
|
||||
nodePaths [][]byte // Paths of all nodes to delete
|
||||
rootHash common.Hash // Hash of the original subtree root (for verification)
|
||||
}
|
||||
|
||||
// Archiver handles the archival process of trie nodes.
|
||||
|
|
@ -79,7 +81,7 @@ func NewArchiver(db ethdb.Database, triedb database.NodeDatabase,
|
|||
}
|
||||
|
||||
// ProcessState archives subtrees from the given state root.
|
||||
// It processes the account trie first, then all storage tries.
|
||||
// It processes storage tries first, then the account trie.
|
||||
func (a *Archiver) ProcessState(root common.Hash) error {
|
||||
a.stateRoot = root
|
||||
|
||||
|
|
@ -141,7 +143,12 @@ func (a *Archiver) processTrie(owner common.Hash, t *Trie) error {
|
|||
subtrees := a.findHeight3Subtrees(t.root, nil, owner)
|
||||
log.Info("Found subtrees to archive", "owner", owner, "count", len(subtrees))
|
||||
|
||||
for _, info := range subtrees {
|
||||
lastLog := time.Now()
|
||||
for i, info := range subtrees {
|
||||
if time.Since(lastLog) > 30*time.Second {
|
||||
log.Info("Archiving subtrees", "owner", owner, "progress", fmt.Sprintf("%d/%d", i, len(subtrees)), "archived", a.subtreesArchived)
|
||||
lastLog = time.Now()
|
||||
}
|
||||
if err := a.archiveSubtree(info); err != nil {
|
||||
log.Warn("Failed to archive subtree", "path", common.Bytes2Hex(info.path), "err", err)
|
||||
continue
|
||||
|
|
@ -159,54 +166,86 @@ func (a *Archiver) processTrie(owner common.Hash, t *Trie) error {
|
|||
// findHeight3Subtrees recursively finds all subtrees with height == 3.
|
||||
// Height is measured from leaves: leaves=0, their parents=1, etc.
|
||||
func (a *Archiver) findHeight3Subtrees(n node, path []byte, owner common.Hash) []*subtreeInfo {
|
||||
info := a.computeSubtreeInfo(n, path, owner)
|
||||
info, err := a.computeSubtreeInfo(n, path, owner)
|
||||
if err != nil {
|
||||
// computeSubtreeInfo failed (e.g. unresolvable hashNode within the
|
||||
// subtree). We cannot archive this node as-is, but deeper children
|
||||
// may still form valid height-3 subtrees. Recurse into them.
|
||||
log.Debug("computeSubtreeInfo failed, trying children", "path", common.Bytes2Hex(path), "err", err)
|
||||
return a.findSubtreesInChildren(n, path, owner)
|
||||
}
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If this subtree has height 3, it's a candidate for archival
|
||||
if info.height == 3 {
|
||||
// Capture the original subtree root hash for verification.
|
||||
// The hash is available from the node that was passed in:
|
||||
// - hashNode: the hash IS the node
|
||||
// - fullNode/shortNode: loaded from DB, flags.hash is set
|
||||
switch nn := n.(type) {
|
||||
case hashNode:
|
||||
info.rootHash = common.BytesToHash(nn)
|
||||
case *fullNode:
|
||||
if nn.flags.hash != nil {
|
||||
info.rootHash = common.BytesToHash(nn.flags.hash)
|
||||
}
|
||||
case *shortNode:
|
||||
if nn.flags.hash != nil {
|
||||
info.rootHash = common.BytesToHash(nn.flags.hash)
|
||||
}
|
||||
}
|
||||
return []*subtreeInfo{info}
|
||||
}
|
||||
|
||||
// If height > 3, recurse into children to find height-3 subtrees
|
||||
if info.height > 3 {
|
||||
var results []*subtreeInfo
|
||||
switch n := n.(type) {
|
||||
case *fullNode:
|
||||
for i, child := range n.Children[:16] {
|
||||
if child != nil {
|
||||
childPath := append(append([]byte{}, path...), byte(i))
|
||||
results = append(results, a.findHeight3Subtrees(child, childPath, owner)...)
|
||||
}
|
||||
}
|
||||
case *shortNode:
|
||||
childPath := append(append([]byte{}, path...), n.Key...)
|
||||
results = append(results, a.findHeight3Subtrees(n.Val, childPath, owner)...)
|
||||
case hashNode:
|
||||
// Resolve and recurse
|
||||
resolved, err := a.resolveNode(n, path, owner)
|
||||
if err == nil {
|
||||
results = append(results, a.findHeight3Subtrees(resolved, path, owner)...)
|
||||
}
|
||||
}
|
||||
return results
|
||||
return a.findSubtreesInChildren(n, path, owner)
|
||||
}
|
||||
|
||||
// Height < 3: no archivable subtrees here
|
||||
return nil
|
||||
}
|
||||
|
||||
// findSubtreesInChildren recurses into the children of a node to find
|
||||
// height-3 subtrees. Used both by the normal height > 3 path and as a
|
||||
// fallback when computeSubtreeInfo fails for a node.
|
||||
func (a *Archiver) findSubtreesInChildren(n node, path []byte, owner common.Hash) []*subtreeInfo {
|
||||
var results []*subtreeInfo
|
||||
switch n := n.(type) {
|
||||
case *fullNode:
|
||||
for i, child := range n.Children[:16] {
|
||||
if child != nil {
|
||||
childPath := append(append([]byte{}, path...), byte(i))
|
||||
results = append(results, a.findHeight3Subtrees(child, childPath, owner)...)
|
||||
}
|
||||
}
|
||||
case *shortNode:
|
||||
childPath := append(append([]byte{}, path...), n.Key...)
|
||||
results = append(results, a.findHeight3Subtrees(n.Val, childPath, owner)...)
|
||||
case hashNode:
|
||||
// Resolve and recurse
|
||||
resolved, err := a.resolveNode(n, path, owner)
|
||||
if err == nil {
|
||||
results = append(results, a.findHeight3Subtrees(resolved, path, owner)...)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// computeSubtreeInfo computes height and collects leaves for a subtree.
|
||||
// Returns nil if the node is nil or an error occurs during resolution.
|
||||
func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *subtreeInfo {
|
||||
// Returns (nil, nil) if the node is nil, already expired, or has no leaves.
|
||||
// Returns (nil, error) if any constituent node could not be resolved — the
|
||||
// caller MUST NOT archive a subtree when an error is returned, as the leaf
|
||||
// set would be incomplete.
|
||||
func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) (*subtreeInfo, error) {
|
||||
switch n := n.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
return nil, nil
|
||||
|
||||
case valueNode:
|
||||
// Leaf: height 0
|
||||
// Encode the leaf as a shortNode for archive storage
|
||||
return &subtreeInfo{
|
||||
path: copyBytes(path),
|
||||
owner: owner,
|
||||
|
|
@ -216,13 +255,16 @@ func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *s
|
|||
Value: []byte(n),
|
||||
}},
|
||||
nodePaths: [][]byte{copyBytes(path)},
|
||||
}
|
||||
}, nil
|
||||
|
||||
case *shortNode:
|
||||
childPath := append(append([]byte{}, path...), n.Key...)
|
||||
childInfo := a.computeSubtreeInfo(n.Val, childPath, owner)
|
||||
childInfo, err := a.computeSubtreeInfo(n.Val, childPath, owner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shortNode key=%x: %w", n.Key, err)
|
||||
}
|
||||
if childInfo == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Adjust relative paths in leaves to include this node's key
|
||||
|
|
@ -236,7 +278,7 @@ func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *s
|
|||
height: childInfo.height + 1,
|
||||
leaves: childInfo.leaves,
|
||||
nodePaths: append([][]byte{copyBytes(path)}, childInfo.nodePaths...),
|
||||
}
|
||||
}, nil
|
||||
|
||||
case *fullNode:
|
||||
var (
|
||||
|
|
@ -247,7 +289,10 @@ func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *s
|
|||
for i, child := range n.Children[:16] {
|
||||
if child != nil {
|
||||
childPath := append(append([]byte{}, path...), byte(i))
|
||||
childInfo := a.computeSubtreeInfo(child, childPath, owner)
|
||||
childInfo, err := a.computeSubtreeInfo(child, childPath, owner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fullNode child[%x]: %w", i, err)
|
||||
}
|
||||
if childInfo != nil {
|
||||
if childInfo.height+1 > maxHeight {
|
||||
maxHeight = childInfo.height + 1
|
||||
|
|
@ -263,7 +308,7 @@ func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *s
|
|||
}
|
||||
|
||||
if len(allLeaves) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &subtreeInfo{
|
||||
|
|
@ -272,21 +317,20 @@ func (a *Archiver) computeSubtreeInfo(n node, path []byte, owner common.Hash) *s
|
|||
height: maxHeight,
|
||||
leaves: allLeaves,
|
||||
nodePaths: allPaths,
|
||||
}
|
||||
}, nil
|
||||
|
||||
case hashNode:
|
||||
resolved, err := a.resolveNode(n, path, owner)
|
||||
if err != nil {
|
||||
log.Debug("Failed to resolve hashNode", "path", common.Bytes2Hex(path), "err", err)
|
||||
return nil
|
||||
return nil, fmt.Errorf("failed to resolve hashNode at path %s: %w", common.Bytes2Hex(path), err)
|
||||
}
|
||||
return a.computeSubtreeInfo(resolved, path, owner)
|
||||
|
||||
case *expiredNode:
|
||||
// Already archived, skip
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// archiveSubtree writes leaves to archive and replaces subtree with expiredNode.
|
||||
|
|
@ -312,7 +356,25 @@ func (a *Archiver) archiveSubtree(info *subtreeInfo) error {
|
|||
return fmt.Errorf("failed to sync archive: %w", err)
|
||||
}
|
||||
|
||||
// 3. Batch database operations
|
||||
// 3. Verify archive round-trip: reconstruct trie from records and
|
||||
// check that the hash matches the original subtree root. This
|
||||
// catches any data corruption before we delete the original nodes.
|
||||
if info.rootHash != (common.Hash{}) {
|
||||
reconstructed, err := archiveRecordsToNode(info.leaves)
|
||||
if err != nil {
|
||||
return fmt.Errorf("archive verification failed: cannot reconstruct trie from records: %w", err)
|
||||
}
|
||||
h := newHasher(false)
|
||||
gotHash := common.BytesToHash(h.hash(reconstructed, true))
|
||||
returnHasherToPool(h)
|
||||
if gotHash != info.rootHash {
|
||||
return fmt.Errorf("archive verification failed: hash mismatch at path %s owner %s: got %s want %s (leaves=%d offset=%d size=%d)",
|
||||
common.Bytes2Hex(info.path), info.owner, gotHash, info.rootHash,
|
||||
len(info.leaves), offset, size)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Batch database operations
|
||||
batch := a.db.NewBatch()
|
||||
|
||||
// Delete all nodes in subtree (except the root which we'll overwrite)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ func (c *committer) commit(path []byte, n node, parallel bool) node {
|
|||
return cn
|
||||
case hashNode:
|
||||
return cn
|
||||
case *expiredNode:
|
||||
return cn
|
||||
default:
|
||||
// nil, valuenode shouldn't be committed
|
||||
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package trie
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -33,11 +34,12 @@ const expiredNodeMarker = 0x00
|
|||
type expiredNode struct {
|
||||
offset uint64
|
||||
size uint64
|
||||
cachedHash hashNode
|
||||
archiveResolver archive.ResolverFn
|
||||
}
|
||||
|
||||
func (n *expiredNode) cache() (hashNode, bool) {
|
||||
return nil, true
|
||||
return n.cachedHash, n.cachedHash == nil
|
||||
}
|
||||
|
||||
func (n *expiredNode) encode(w rlp.EncoderBuffer) {
|
||||
|
|
@ -62,36 +64,101 @@ func (n *expiredNode) SetArchiveResolver(resolver archive.ResolverFn) {
|
|||
n.archiveResolver = resolver
|
||||
}
|
||||
|
||||
// resolveExpiredNodeData resolves an expired node from the archive, verifies
|
||||
// the reconstructed subtree hash, and stamps the cached hash onto the root.
|
||||
// Returns an error if the archive data is corrupted (hash mismatch).
|
||||
func resolveExpiredNodeData(n *expiredNode) (node, error) {
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve expired node: %w", err)
|
||||
}
|
||||
resolved, err := archiveRecordsToNode(records)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to rebuild expired node from archive: %w", err)
|
||||
}
|
||||
// Verify hash integrity: if the original hash is known, check that the
|
||||
// reconstructed subtree produces the same hash. A mismatch means the
|
||||
// archive is corrupted (e.g. missing leaves due to unresolvable hashNodes
|
||||
// during archival) and any data from it is unreliable.
|
||||
if n.cachedHash != nil {
|
||||
h := newHasher(false)
|
||||
gotHash := h.hash(resolved, true)
|
||||
returnHasherToPool(h)
|
||||
if !bytes.Equal(gotHash, n.cachedHash) {
|
||||
return nil, fmt.Errorf("expired node hash mismatch at offset=%d size=%d: archive data is corrupted (expected %x got %x, %d records)",
|
||||
n.offset, n.size, []byte(n.cachedHash), gotHash, len(records))
|
||||
}
|
||||
// Stamp the original hash onto the resolved subtree root so the
|
||||
// hasher returns it directly instead of re-computing.
|
||||
switch nn := resolved.(type) {
|
||||
case *fullNode:
|
||||
nn.flags.hash = n.cachedHash
|
||||
case *shortNode:
|
||||
nn.flags.hash = n.cachedHash
|
||||
}
|
||||
}
|
||||
// Mark the entire resolved subtree as dirty. This is critical for
|
||||
// correctness with pathdb's diff layer model: when a trie with expired
|
||||
// nodes is modified and committed, the committer only captures dirty
|
||||
// nodes into the NodeSet (which becomes the diff layer). Without this
|
||||
// marking, resolved-but-unmodified sibling nodes within the subtree
|
||||
// would exist nowhere — not in any diff layer (they're clean) and not
|
||||
// in the raw DB (the archiver deleted them). Subsequent trie accesses
|
||||
// from higher diff layers would fall through to the disk layer, find
|
||||
// nothing, and produce MissingNodeError.
|
||||
//
|
||||
// For read-only tries (only get operations, no commit), this dirty
|
||||
// marking is harmless — the nodes are discarded when the trie is GC'd.
|
||||
markSubtreeDirty(resolved)
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// markSubtreeDirty recursively marks all fullNode and shortNode in the
|
||||
// subtree as dirty, preserving any cached hashes. This ensures the
|
||||
// committer will capture them in the NodeSet during trie commit.
|
||||
func markSubtreeDirty(n node) {
|
||||
switch n := n.(type) {
|
||||
case *fullNode:
|
||||
n.flags.dirty = true
|
||||
for _, child := range n.Children[:16] {
|
||||
if child != nil {
|
||||
markSubtreeDirty(child)
|
||||
}
|
||||
}
|
||||
case *shortNode:
|
||||
n.flags.dirty = true
|
||||
markSubtreeDirty(n.Val)
|
||||
}
|
||||
// valueNode, hashNode, nil: no flags to mark
|
||||
}
|
||||
|
||||
func archiveRecordsToNode(records []*archive.Record) (node, error) {
|
||||
if len(records) == 0 {
|
||||
return nil, archive.EmptyArchiveRecord
|
||||
}
|
||||
if len(records) == 1 {
|
||||
return buildLeafFromRecord(records[0])
|
||||
}
|
||||
|
||||
var newnode fullNode
|
||||
// Build the trie incrementally from nil to produce the canonical
|
||||
// MPT structure. Starting with a fullNode would be wrong when the
|
||||
// original subtree root was a shortNode (shared prefix).
|
||||
var root node
|
||||
for i, record := range records {
|
||||
if err := validateRecordPath(record.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we are not in the case of a single leaf node, so each
|
||||
// path should be at least 2 nibbles (terminator included)
|
||||
if len(record.Path) < 2 || !hasTerm(record.Path) {
|
||||
return nil, fmt.Errorf("invalid record path for non-leaf node #%d: %v", i, record.Path)
|
||||
}
|
||||
key, err := normalizeRecordKey(record.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
child, err := insertTrieNode(newnode.Children[key[0]], key[1:], valueNode(record.Value))
|
||||
if len(key) < 1 {
|
||||
return nil, fmt.Errorf("empty key in record #%d", i)
|
||||
}
|
||||
root, err = insertTrieNode(root, key, valueNode(record.Value))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newnode.Children[key[0]] = child
|
||||
}
|
||||
return &newnode, nil
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func validateRecordPath(path []byte) error {
|
||||
|
|
@ -106,14 +173,6 @@ func validateRecordPath(path []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func buildLeafFromRecord(record *archive.Record) (node, error) {
|
||||
key, err := normalizeRecordKey(record.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &shortNode{Key: key, Val: valueNode(record.Value)}, nil
|
||||
}
|
||||
|
||||
// normalizeRecordKey ensures the record path is a hex-nibble key suitable for
|
||||
// leaf insertion by guaranteeing a single terminator nibble and preserving any
|
||||
// already-terminated path. Empty paths are normalized to a sole terminator.
|
||||
|
|
|
|||
|
|
@ -19,12 +19,45 @@ package trie
|
|||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie/archive"
|
||||
)
|
||||
|
||||
// setupTestArchive creates a temporary archive directory with an archive file
|
||||
// containing the given records, and configures archive.ArchiveDataDir to point
|
||||
// to it. It returns the offset and size of the written data, and a cleanup function.
|
||||
func setupTestArchive(t *testing.T, records []*archive.Record) (offset, size uint64, cleanup func()) {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
gethDir := filepath.Join(tmpDir, "geth")
|
||||
if err := os.MkdirAll(gethDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writer, err := archive.NewArchiveWriter(filepath.Join(gethDir, "nodearchive"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
offset, size, err = writer.WriteSubtree(records)
|
||||
if err != nil {
|
||||
writer.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
oldDir := archive.ArchiveDataDir
|
||||
archive.ArchiveDataDir = tmpDir
|
||||
|
||||
return offset, size, func() {
|
||||
archive.ArchiveDataDir = oldDir
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredNodeEncodeDecode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
offset uint64
|
||||
|
|
@ -120,34 +153,36 @@ func TestExpiredNodeInvalidLength(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestExpiredNodeNoResolver(t *testing.T) {
|
||||
func TestExpiredNodeNoArchiveFile(t *testing.T) {
|
||||
// When no archive file exists, Get should return an error
|
||||
tmpDir := t.TempDir()
|
||||
gethDir := filepath.Join(tmpDir, "geth")
|
||||
if err := os.MkdirAll(gethDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldDir := archive.ArchiveDataDir
|
||||
archive.ArchiveDataDir = tmpDir
|
||||
defer func() { archive.ArchiveDataDir = oldDir }()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.root = &expiredNode{offset: 100}
|
||||
tr.root = &expiredNode{offset: 100, size: 50}
|
||||
|
||||
_, err := tr.Get([]byte("key"))
|
||||
if !errors.Is(err, archive.ErrNoResolver) {
|
||||
t.Errorf("expected archive.ErrNoResolver, got %v", err)
|
||||
if err == nil {
|
||||
t.Error("expected error when archive file doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredNodeWithResolver(t *testing.T) {
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
|
||||
leafNode := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("testvalue")),
|
||||
}
|
||||
encodedLeaf := nodeToBytes(leafNode)
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
if offset == 100 {
|
||||
return []*archive.Record{{Value: encodedLeaf}}, nil
|
||||
}
|
||||
return nil, errors.New("unknown offset")
|
||||
}
|
||||
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: uint64(len(encodedLeaf)), archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
val, err := tr.Get([]byte{0x12})
|
||||
if err != nil {
|
||||
|
|
@ -159,14 +194,10 @@ func TestExpiredNodeWithResolver(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeCopy(t *testing.T) {
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
original := &expiredNode{
|
||||
offset: 12345,
|
||||
size: 6789,
|
||||
archiveResolver: resolver,
|
||||
archiveResolver: archive.ArchivedNodeResolver,
|
||||
}
|
||||
|
||||
copied := copyNode(original)
|
||||
|
|
@ -201,18 +232,9 @@ func TestArchiveRecordsToNodeEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestArchiveRecordsToNodeMultiple(t *testing.T) {
|
||||
leaf1 := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x10})),
|
||||
Val: valueNode([]byte("value1")),
|
||||
}
|
||||
leaf2 := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x20})),
|
||||
Val: valueNode([]byte("value2")),
|
||||
}
|
||||
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01}, Value: nodeToBytes(leaf1)},
|
||||
{Path: []byte{0x02}, Value: nodeToBytes(leaf2)},
|
||||
{Path: []byte{0x01, 16}, Value: []byte("value1")},
|
||||
{Path: []byte{0x02, 16}, Value: []byte("value2")},
|
||||
}
|
||||
|
||||
node, err := archiveRecordsToNode(records)
|
||||
|
|
@ -234,27 +256,17 @@ func TestArchiveRecordsToNodeMultiple(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeGetMultipleRecords(t *testing.T) {
|
||||
leaf1 := &shortNode{
|
||||
Key: hexToCompact([]byte{0x02, 0x03, 0x04, 16}),
|
||||
Val: valueNode([]byte("value1")),
|
||||
}
|
||||
leaf2 := &shortNode{
|
||||
Key: hexToCompact([]byte{0x05, 0x06, 0x07, 16}),
|
||||
Val: valueNode([]byte("value2")),
|
||||
}
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{0x01}, Value: nodeToBytes(leaf1)},
|
||||
{Path: []byte{0x04}, Value: nodeToBytes(leaf2)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
|
||||
{Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
val, err := tr.Get([]byte{0x12, 0x34})
|
||||
val, err := tr.Get([]byte{0x12})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -263,10 +275,9 @@ func TestExpiredNodeGetMultipleRecords(t *testing.T) {
|
|||
}
|
||||
|
||||
tr2 := NewEmpty(nil)
|
||||
tr2.SetArchiveResolver(resolver)
|
||||
tr2.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr2.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
val2, err := tr2.Get([]byte{0x45, 0x67})
|
||||
val2, err := tr2.Get([]byte{0x45})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -276,20 +287,14 @@ func TestExpiredNodeGetMultipleRecords(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeGetKeyNotFound(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("value1")),
|
||||
}
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{0x01}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
val, err := tr.Get([]byte{0xff, 0xff})
|
||||
if err != nil {
|
||||
|
|
@ -301,20 +306,14 @@ func TestExpiredNodeGetKeyNotFound(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeGetPathMismatch(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("testvalue")),
|
||||
}
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{0x01}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
val, err := tr.Get([]byte{0x19})
|
||||
if err != nil {
|
||||
|
|
@ -326,20 +325,14 @@ func TestExpiredNodeGetPathMismatch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeInsert(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("existing")),
|
||||
}
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("existing")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
err := tr.Update([]byte{0x45}, []byte("newvalue"))
|
||||
if err != nil {
|
||||
|
|
@ -356,20 +349,14 @@ func TestExpiredNodeInsert(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeUpdate(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("oldvalue")),
|
||||
}
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("oldvalue")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
err := tr.Update([]byte{0x12}, []byte("newvalue"))
|
||||
if err != nil {
|
||||
|
|
@ -386,28 +373,15 @@ func TestExpiredNodeUpdate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpiredNodeDelete(t *testing.T) {
|
||||
leaf1 := &shortNode{
|
||||
Key: hexToCompact([]byte{0x02, 16}),
|
||||
Val: valueNode([]byte("value1")),
|
||||
}
|
||||
leaf2 := &shortNode{
|
||||
Key: hexToCompact([]byte{0x05, 16}),
|
||||
Val: valueNode([]byte("value2")),
|
||||
}
|
||||
|
||||
branch := &fullNode{}
|
||||
branch.Children[0x01] = leaf1
|
||||
branch.Children[0x04] = leaf2
|
||||
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
return []*archive.Record{
|
||||
{Path: []byte{}, Value: nodeToBytes(branch)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
|
||||
{Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
err := tr.Delete([]byte{0x12})
|
||||
if err != nil {
|
||||
|
|
@ -432,22 +406,14 @@ func TestExpiredNodeDelete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTrieCopyPreservesArchiveResolver(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("testvalue")),
|
||||
}
|
||||
|
||||
resolverCalled := false
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
resolverCalled = true
|
||||
return []*archive.Record{
|
||||
{Path: []byte{}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
trCopy := tr.Copy()
|
||||
|
||||
|
|
@ -455,36 +421,180 @@ func TestTrieCopyPreservesArchiveResolver(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !resolverCalled {
|
||||
t.Error("resolver was not called on copied trie")
|
||||
}
|
||||
if string(val) != "testvalue" {
|
||||
t.Errorf("value mismatch: got %q, want %q", val, "testvalue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredNodeGetNode(t *testing.T) {
|
||||
leaf := &shortNode{
|
||||
Key: hexToCompact(keybytesToHex([]byte{0x12})),
|
||||
Val: valueNode([]byte("testvalue")),
|
||||
}
|
||||
|
||||
resolverCalled := false
|
||||
resolver := func(offset, size uint64) ([]*archive.Record, error) {
|
||||
resolverCalled = true
|
||||
return []*archive.Record{
|
||||
{Path: []byte{}, Value: nodeToBytes(leaf)},
|
||||
}, nil
|
||||
func TestWalkWithExpiredNodes(t *testing.T) {
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
|
||||
{Path: []byte{0x04, 0x05, 16}, Value: []byte("value2")},
|
||||
{Path: []byte{0x07, 0x08, 16}, Value: []byte("value3")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.SetArchiveResolver(resolver)
|
||||
tr.root = &expiredNode{offset: 100, size: 200, archiveResolver: resolver}
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
var leaves []string
|
||||
stats, err := tr.Walk(func(path []byte, value []byte) error {
|
||||
leaves = append(leaves, string(value))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Walk failed: %v", err)
|
||||
}
|
||||
if stats.Leaves != 3 {
|
||||
t.Errorf("expected 3 leaves, got %d", stats.Leaves)
|
||||
}
|
||||
if stats.ExpiredResolved != 1 {
|
||||
t.Errorf("expected 1 expired resolved, got %d", stats.ExpiredResolved)
|
||||
}
|
||||
// Verify all values were visited
|
||||
expected := map[string]bool{"value1": true, "value2": true, "value3": true}
|
||||
for _, leaf := range leaves {
|
||||
if !expected[leaf] {
|
||||
t.Errorf("unexpected leaf value: %q", leaf)
|
||||
}
|
||||
delete(expected, leaf)
|
||||
}
|
||||
if len(expected) > 0 {
|
||||
t.Errorf("missing leaves: %v", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkEmptyTrie(t *testing.T) {
|
||||
tr := NewEmpty(nil)
|
||||
stats, err := tr.Walk(func(path []byte, value []byte) error {
|
||||
t.Error("callback should not be called for empty trie")
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Walk failed: %v", err)
|
||||
}
|
||||
if stats.Leaves != 0 || stats.ExpiredResolved != 0 {
|
||||
t.Errorf("expected zero stats for empty trie, got leaves=%d expired=%d", stats.Leaves, stats.ExpiredResolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkCallbackError(t *testing.T) {
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("value1")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
testErr := errors.New("test error")
|
||||
_, err := tr.Walk(func(path []byte, value []byte) error {
|
||||
return testErr
|
||||
})
|
||||
if !errors.Is(err, testErr) {
|
||||
t.Fatalf("expected test error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpiredNodeResolvedSubtreeDirty verifies that when an expired node is
|
||||
// resolved and a sibling leaf is modified, the commit captures ALL resolved
|
||||
// nodes (not just the modified path). Without this fix, resolved-but-unmodified
|
||||
// nodes would be lost: not in the diff layer (clean) and not in the raw DB
|
||||
// (deleted by archiver).
|
||||
func TestExpiredNodeResolvedSubtreeDirty(t *testing.T) {
|
||||
// Use large values (>32 bytes) so leaf nodes are NOT embedded in
|
||||
// their parent. This matches production storage tries where
|
||||
// intermediate nodes are large enough to be stored independently.
|
||||
bigVal1 := bytes.Repeat([]byte("A"), 40)
|
||||
bigVal2 := bytes.Repeat([]byte("B"), 40)
|
||||
|
||||
// Create an archive with records under different branches.
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: bigVal1},
|
||||
{Path: []byte{0x04, 0x05, 16}, Value: bigVal2},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
// Insert a value that goes through one branch of the resolved subtree.
|
||||
// This modifies path [1, ...] but leaves path [4, ...] unmodified.
|
||||
if err := tr.Update([]byte{0x12}, bytes.Repeat([]byte("C"), 40)); err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
|
||||
// Commit the trie. The NodeSet should be non-nil because we modified data.
|
||||
_, nodes := tr.Commit(false)
|
||||
if nodes == nil {
|
||||
t.Fatal("expected non-nil NodeSet after modifying expired subtree")
|
||||
}
|
||||
|
||||
// The resolved-but-unmodified sibling (path [4, 5]) should also be
|
||||
// captured in the NodeSet, because markSubtreeDirty ensures all resolved
|
||||
// nodes are dirty. Count the nodes to verify.
|
||||
nodeCount := len(nodes.Nodes)
|
||||
// We expect at least 3 nodes: the root, the modified branch, and the
|
||||
// sibling branch. The exact count depends on trie structure.
|
||||
if nodeCount < 3 {
|
||||
t.Errorf("expected at least 3 nodes in NodeSet (root + modified + sibling), got %d", nodeCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkSubtreeDirty verifies that markSubtreeDirty correctly sets the dirty
|
||||
// flag on all nodes in a subtree while preserving cached hashes.
|
||||
func TestMarkSubtreeDirty(t *testing.T) {
|
||||
// Build a small trie structure
|
||||
leaf1 := &shortNode{Key: []byte{1, 16}, Val: valueNode("v1")}
|
||||
leaf2 := &shortNode{Key: []byte{2, 16}, Val: valueNode("v2")}
|
||||
branch := &fullNode{}
|
||||
branch.Children[1] = leaf1
|
||||
branch.Children[2] = leaf2
|
||||
|
||||
// Set hash but not dirty (as if loaded from DB)
|
||||
branch.flags = nodeFlag{hash: hashNode("testhash"), dirty: false}
|
||||
leaf1.flags = nodeFlag{hash: hashNode("hash1"), dirty: false}
|
||||
leaf2.flags = nodeFlag{hash: hashNode("hash2"), dirty: false}
|
||||
|
||||
markSubtreeDirty(branch)
|
||||
|
||||
// All nodes should be dirty
|
||||
if !branch.flags.dirty {
|
||||
t.Error("branch should be dirty")
|
||||
}
|
||||
if !leaf1.flags.dirty {
|
||||
t.Error("leaf1 should be dirty")
|
||||
}
|
||||
if !leaf2.flags.dirty {
|
||||
t.Error("leaf2 should be dirty")
|
||||
}
|
||||
|
||||
// Hashes should be preserved
|
||||
if !bytes.Equal(branch.flags.hash, hashNode("testhash")) {
|
||||
t.Error("branch hash should be preserved")
|
||||
}
|
||||
if !bytes.Equal(leaf1.flags.hash, hashNode("hash1")) {
|
||||
t.Error("leaf1 hash should be preserved")
|
||||
}
|
||||
if !bytes.Equal(leaf2.flags.hash, hashNode("hash2")) {
|
||||
t.Error("leaf2 hash should be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredNodeGetNode(t *testing.T) {
|
||||
records := []*archive.Record{
|
||||
{Path: []byte{0x01, 0x02, 16}, Value: []byte("testvalue")},
|
||||
}
|
||||
offset, size, cleanup := setupTestArchive(t, records)
|
||||
defer cleanup()
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
tr.root = &expiredNode{offset: offset, size: size}
|
||||
|
||||
_, _, err := tr.GetNode(hexToCompact([]byte{0x01, 0x02}))
|
||||
if !resolverCalled {
|
||||
t.Error("resolver was not called during GetNode")
|
||||
}
|
||||
if err != nil && err.Error() != "non-consensus node" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package trie
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
|
|
@ -97,6 +98,22 @@ func (h *hasher) hash(n node, force bool) []byte {
|
|||
// hash nodes don't have children, so they're left as were
|
||||
return n
|
||||
|
||||
case *expiredNode:
|
||||
// Return the original subtree hash that was cached when the
|
||||
// expired node was decoded. The parent node references this
|
||||
// hash, so we must return the same value to keep the Merkle
|
||||
// root consistent.
|
||||
if n.cachedHash != nil {
|
||||
return n.cachedHash
|
||||
}
|
||||
// Fallback: hash the marker blob (should not happen in practice
|
||||
// because decodeNodeUnsafe always provides the hash).
|
||||
var buf [1 + 2*8]byte // 17 bytes
|
||||
buf[0] = expiredNodeMarker
|
||||
binary.BigEndian.PutUint64(buf[1:], n.offset)
|
||||
binary.BigEndian.PutUint64(buf[9:], n.size)
|
||||
return h.hashData(buf[:])
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected node type, %T", n))
|
||||
}
|
||||
|
|
@ -214,6 +231,12 @@ func (h *hasher) proofHash(original node) []byte {
|
|||
return bytes.Clone(h.encodeShortNode(n))
|
||||
case *fullNode:
|
||||
return bytes.Clone(h.encodeFullNode(n))
|
||||
case *expiredNode:
|
||||
var buf [1 + 2*8]byte
|
||||
buf[0] = expiredNodeMarker
|
||||
binary.BigEndian.PutUint64(buf[1:], n.offset)
|
||||
binary.BigEndian.PutUint64(buf[9:], n.size)
|
||||
return buf[:]
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected node type, %T", original))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie/archive"
|
||||
)
|
||||
|
|
@ -166,7 +167,8 @@ func decodeNodeUnsafe(hash, buf []byte) (node, error) {
|
|||
}
|
||||
offset := binary.BigEndian.Uint64(buf[1:])
|
||||
size := binary.BigEndian.Uint64(buf[1+archive.OffsetSize:])
|
||||
return &expiredNode{offset: offset, size: size, archiveResolver: archive.ArchivedNodeResolver}, nil
|
||||
log.Debug("Decoded expired node", "offset", offset, "size", size, "hash", common.BytesToHash(hash))
|
||||
return &expiredNode{offset: offset, size: size, cachedHash: hashNode(hash), archiveResolver: archive.ArchivedNodeResolver}, nil
|
||||
}
|
||||
elems, _, err := rlp.SplitList(buf)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/trie/archive"
|
||||
)
|
||||
|
||||
// Prove constructs a merkle proof for key. The result contains all encoded nodes
|
||||
|
|
@ -78,6 +79,16 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
|
|||
// clean cache or the database, they are all in their own
|
||||
// copy and safe to use unsafe decoder.
|
||||
tn = mustDecodeNodeUnsafe(n, blob)
|
||||
case *expiredNode:
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve expired node in proof: %w", err)
|
||||
}
|
||||
resolved, err := archiveRecordsToNode(records)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rebuild expired node in proof: %w", err)
|
||||
}
|
||||
tn = resolved
|
||||
default:
|
||||
panic(fmt.Sprintf("%T: invalid node: %v", tn, tn))
|
||||
}
|
||||
|
|
@ -617,6 +628,8 @@ func get(tn node, key []byte, skipResolved bool) ([]byte, node) {
|
|||
}
|
||||
case hashNode:
|
||||
return key, n
|
||||
case *expiredNode:
|
||||
return key, n
|
||||
case nil:
|
||||
return key, nil
|
||||
case valueNode:
|
||||
|
|
|
|||
121
trie/trie.go
121
trie/trie.go
|
|
@ -230,32 +230,12 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no
|
|||
value, newnode, _, err := t.get(child, key, pos)
|
||||
return value, newnode, true, err
|
||||
case *expiredNode:
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
if err != nil {
|
||||
return nil, n, false, fmt.Errorf("failed to resolve expired node: %w", err)
|
||||
}
|
||||
newnode, err := archiveRecordsToNode(records)
|
||||
// alternative: don't rebuild, just find the value
|
||||
// for _, record := range records {
|
||||
// // make sure that the path up to the node matches
|
||||
// if bytes.HasPrefix(key[pos:], record.Path) {
|
||||
// resolved, err := decodeNodeUnsafe(nil, record.Value)
|
||||
// if err != nil {
|
||||
// fmt.Printf("%v %x\n", record.Path, record.Value)
|
||||
// return nil, n, false, fmt.Errorf("failed to deserialize RLP node: %w", err)
|
||||
// }
|
||||
// if leaf, ok := resolved.(*shortNode); ok {
|
||||
// // make sure that the key to the leaf also matches
|
||||
// if bytes.Equal(key[pos+len(record.Path):], leaf.Key) {
|
||||
// return leaf.Val.(valueNode), newnode, true, nil
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
log.Debug("Resolving expired node in get()", "owner", t.owner, "offset", n.offset, "size", n.size, "pos", pos)
|
||||
newnode, err := resolveExpiredNodeData(n)
|
||||
if err != nil {
|
||||
return nil, n, false, err
|
||||
}
|
||||
value, _, _, err = t.get(newnode, key, pos+1)
|
||||
value, _, _, err = t.get(newnode, key, pos)
|
||||
return value, newnode, true, err
|
||||
default:
|
||||
panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
|
||||
|
|
@ -392,12 +372,11 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
|
|||
return item, newnode, resolved + 1, err
|
||||
|
||||
case *expiredNode:
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
rn, err := resolveExpiredNodeData(n)
|
||||
if err != nil {
|
||||
return nil, n, 0, fmt.Errorf("failed to resolve expired node: %w", err)
|
||||
return nil, n, 0, err
|
||||
}
|
||||
newnode, err := archiveRecordsToNode(records)
|
||||
item, newnode, resolvedCount, err := t.getNode(newnode, path, pos)
|
||||
item, newnode, resolvedCount, err := t.getNode(rn, path, pos)
|
||||
return item, newnode, resolvedCount + 1, err
|
||||
|
||||
default:
|
||||
|
|
@ -524,16 +503,16 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
|
|||
return true, nn, nil
|
||||
|
||||
case *expiredNode:
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
log.Info("Resolving expired node in insert()", "owner", t.owner, "offset", n.offset, "size", n.size)
|
||||
rn, err := resolveExpiredNodeData(n)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to resolve expired node: %w", err)
|
||||
return false, nil, err
|
||||
}
|
||||
nn, err := archiveRecordsToNode(records)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to rebuild expired node from archive: %w", err)
|
||||
dirty, nn, err := t.insert(rn, prefix, key, value)
|
||||
if !dirty || err != nil {
|
||||
return false, rn, err
|
||||
}
|
||||
dirty, nn, err := t.insert(nn, prefix, key, value)
|
||||
return dirty && err == nil, nn, err
|
||||
return true, nn, nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
||||
|
|
@ -697,16 +676,16 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
|
|||
return true, nn, nil
|
||||
|
||||
case *expiredNode:
|
||||
records, err := archive.ArchivedNodeResolver(n.offset, n.size)
|
||||
log.Info("Resolving expired node in delete()", "owner", t.owner, "offset", n.offset, "size", n.size)
|
||||
rn, err := resolveExpiredNodeData(n)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to resolve expired node: %w", err)
|
||||
return false, nil, err
|
||||
}
|
||||
nn, err := archiveRecordsToNode(records)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to rebuild expired node from archive: %w", err)
|
||||
dirty, nn, err := t.delete(rn, prefix, key)
|
||||
if !dirty || err != nil {
|
||||
return false, rn, err
|
||||
}
|
||||
dirty, _, err := t.delete(nn, prefix, key)
|
||||
return dirty && err == nil, nn, err
|
||||
return true, nn, nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key))
|
||||
|
|
@ -742,7 +721,8 @@ func copyNode(n node) node {
|
|||
return &expiredNode{
|
||||
offset: n.offset,
|
||||
size: n.size,
|
||||
archiveResolver: archive.ArchivedNodeResolver,
|
||||
cachedHash: common.CopyBytes(n.cachedHash),
|
||||
archiveResolver: n.archiveResolver,
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("%T: unknown node type", n))
|
||||
|
|
@ -750,8 +730,11 @@ func copyNode(n node) node {
|
|||
}
|
||||
|
||||
func (t *Trie) resolve(n node, prefix []byte) (node, error) {
|
||||
if n, ok := n.(hashNode); ok {
|
||||
switch n := n.(type) {
|
||||
case hashNode:
|
||||
return t.resolveAndTrack(n, prefix)
|
||||
case *expiredNode:
|
||||
return resolveExpiredNodeData(n)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
|
@ -862,6 +845,58 @@ func (t *Trie) Witness() map[string][]byte {
|
|||
return t.prevalueTracer.Values()
|
||||
}
|
||||
|
||||
// WalkStats holds statistics from a Walk traversal.
|
||||
type WalkStats struct {
|
||||
Leaves int // Number of leaf nodes visited
|
||||
ExpiredResolved int // Number of expired nodes resolved from archive
|
||||
}
|
||||
|
||||
// Walk recursively traverses the trie, resolving all nodes including
|
||||
// hashNodes and expiredNodes. It calls fn for each leaf found.
|
||||
// This triggers hash verification for expired nodes via cachedHash.
|
||||
func (t *Trie) Walk(fn func(path []byte, value []byte) error) (WalkStats, error) {
|
||||
return t.walk(t.root, nil, fn)
|
||||
}
|
||||
|
||||
func (t *Trie) walk(n node, path []byte, fn func([]byte, []byte) error) (WalkStats, error) {
|
||||
switch n := n.(type) {
|
||||
case *shortNode:
|
||||
return t.walk(n.Val, append(append([]byte{}, path...), n.Key...), fn)
|
||||
case *fullNode:
|
||||
var stats WalkStats
|
||||
for i, child := range n.Children[:16] {
|
||||
if child != nil {
|
||||
childStats, err := t.walk(child, append(append([]byte{}, path...), byte(i)), fn)
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
stats.Leaves += childStats.Leaves
|
||||
stats.ExpiredResolved += childStats.ExpiredResolved
|
||||
}
|
||||
}
|
||||
return stats, nil
|
||||
case hashNode:
|
||||
resolved, err := t.resolveAndTrack(n, path)
|
||||
if err != nil {
|
||||
return WalkStats{}, err
|
||||
}
|
||||
return t.walk(resolved, path, fn)
|
||||
case *expiredNode:
|
||||
resolved, err := resolveExpiredNodeData(n)
|
||||
if err != nil {
|
||||
return WalkStats{}, err
|
||||
}
|
||||
childStats, err := t.walk(resolved, path, fn)
|
||||
childStats.ExpiredResolved++
|
||||
return childStats, err
|
||||
case valueNode:
|
||||
return WalkStats{Leaves: 1}, fn(path, []byte(n))
|
||||
case nil:
|
||||
return WalkStats{}, nil
|
||||
}
|
||||
return WalkStats{}, nil
|
||||
}
|
||||
|
||||
// reset drops the referenced root node and cleans all internal state.
|
||||
func (t *Trie) reset() {
|
||||
t.root = nil
|
||||
|
|
|
|||
|
|
@ -399,6 +399,28 @@ func (db *Database) Disk() ethdb.Database {
|
|||
return db.disk
|
||||
}
|
||||
|
||||
// DiffHead returns the root hash of the topmost diff layer in pathdb.
|
||||
// If there are no diff layers or the backend is not pathdb, it returns
|
||||
// the zero hash and false.
|
||||
func (db *Database) DiffHead() (common.Hash, bool) {
|
||||
pdb, ok := db.backend.(*pathdb.Database)
|
||||
if !ok {
|
||||
return common.Hash{}, false
|
||||
}
|
||||
return pdb.DiffHead()
|
||||
}
|
||||
|
||||
// DisableStateHistory closes and disables the state history freezer.
|
||||
// This is used by the archiver to bypass state history writes during
|
||||
// diff layer flushing when state history may have gaps.
|
||||
func (db *Database) DisableStateHistory() {
|
||||
pdb, ok := db.backend.(*pathdb.Database)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pdb.DisableStateHistory()
|
||||
}
|
||||
|
||||
// SnapshotCompleted returns the indicator if the snapshot is completed.
|
||||
func (db *Database) SnapshotCompleted() bool {
|
||||
pdb, ok := db.backend.(*pathdb.Database)
|
||||
|
|
|
|||
|
|
@ -318,6 +318,30 @@ func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint6
|
|||
return db.tree.cap(root, maxDiffLayers)
|
||||
}
|
||||
|
||||
// DiffHead returns the root hash of the topmost diff layer. If there are no
|
||||
// diff layers (only the disk layer), it returns the disk layer root and false.
|
||||
func (db *Database) DiffHead() (common.Hash, bool) {
|
||||
db.lock.RLock()
|
||||
defer db.lock.RUnlock()
|
||||
|
||||
return db.tree.diffHead()
|
||||
}
|
||||
|
||||
// DisableStateHistory closes and disables the state history freezer. This is
|
||||
// used by the archiver to bypass state history writes during diff layer flushing,
|
||||
// since the archiver only needs trie nodes committed to disk and state history
|
||||
// may have gaps from unclean shutdowns that prevent sequential appends.
|
||||
func (db *Database) DisableStateHistory() {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
||||
if db.stateFreezer != nil {
|
||||
db.stateFreezer.Close()
|
||||
db.stateFreezer = nil
|
||||
log.Info("Disabled state history freezer")
|
||||
}
|
||||
}
|
||||
|
||||
// Commit traverses downwards the layer tree from a specified layer with the
|
||||
// provided state root and all the layers below are flattened downwards. It
|
||||
// can be used alone and mostly for test purposes.
|
||||
|
|
|
|||
|
|
@ -278,9 +278,17 @@ func truncateFromHead(store ethdb.AncientStore, typ historyType, nhead uint64) (
|
|||
return 0, err
|
||||
}
|
||||
// Ensure that the truncation target falls within the valid range.
|
||||
if ohead < nhead || nhead < otail {
|
||||
if nhead < otail {
|
||||
return 0, fmt.Errorf("%w, %s, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, typ, otail, ohead, nhead)
|
||||
}
|
||||
// If the target is ahead of the current head, there's nothing to truncate.
|
||||
// This can happen after unclean shutdowns where the state history was not
|
||||
// fully written.
|
||||
if ohead < nhead {
|
||||
log.Warn("State history shorter than target, nothing to truncate",
|
||||
"type", typ.String(), "head", ohead, "target", nhead)
|
||||
return 0, nil
|
||||
}
|
||||
// Short circuit if nothing to truncate.
|
||||
if ohead == nhead {
|
||||
return 0, nil
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
// of the referenced layer by themselves.
|
||||
type layerTree struct {
|
||||
base *diskLayer
|
||||
head common.Hash // Root hash of the topmost layer (diff or disk)
|
||||
layers map[common.Hash]layer
|
||||
|
||||
// descendants is a two-dimensional map where the keys represent
|
||||
|
|
@ -59,6 +60,7 @@ func (tree *layerTree) init(head layer) {
|
|||
defer tree.lock.Unlock()
|
||||
|
||||
current := head
|
||||
tree.head = head.rootHash()
|
||||
tree.layers = make(map[common.Hash]layer)
|
||||
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})
|
||||
|
||||
|
|
@ -76,6 +78,18 @@ func (tree *layerTree) init(head layer) {
|
|||
tree.lookup = newLookup(head, tree.isDescendant)
|
||||
}
|
||||
|
||||
// diffHead returns the root hash of the topmost diff layer. If there are no
|
||||
// diff layers, returns the disk layer root and false.
|
||||
func (tree *layerTree) diffHead() (common.Hash, bool) {
|
||||
tree.lock.RLock()
|
||||
defer tree.lock.RUnlock()
|
||||
|
||||
if _, ok := tree.layers[tree.head].(*diffLayer); ok {
|
||||
return tree.head, true
|
||||
}
|
||||
return tree.base.rootHash(), false
|
||||
}
|
||||
|
||||
// get retrieves a layer belonging to the given state root.
|
||||
func (tree *layerTree) get(root common.Hash) layer {
|
||||
tree.lock.RLock()
|
||||
|
|
|
|||
Loading…
Reference in a new issue