mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-06 13:11:17 +00:00
trie, cmd/geth: add archiver command
This commit is contained in:
parent
d087178f8c
commit
d82e83d2d9
4 changed files with 698 additions and 0 deletions
201
cmd/geth/archivecmd.go
Normal file
201
cmd/geth/archivecmd.go
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/archive"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Flags for the archive command
|
||||||
|
archiveOutputFlag = &cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Usage: "Path to archive output file",
|
||||||
|
Value: "", // Default: <datadir>/nodearchive
|
||||||
|
}
|
||||||
|
archiveCompactionIntervalFlag = &cli.Uint64Flag{
|
||||||
|
Name: "compaction-interval",
|
||||||
|
Usage: "Run compaction after this many subtrees (0 = disable)",
|
||||||
|
Value: 1000,
|
||||||
|
}
|
||||||
|
archiveDryRunFlag = &cli.BoolFlag{
|
||||||
|
Name: "dry-run",
|
||||||
|
Usage: "Simulate without modifying database",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
archiveCommand = &cli.Command{
|
||||||
|
Name: "archive",
|
||||||
|
Usage: "Archive state trie nodes to reduce database size",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
archiveGenerateCmd,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveGenerateCmd = &cli.Command{
|
||||||
|
Name: "generate",
|
||||||
|
Usage: "Generate archive files from height-3 subtrees",
|
||||||
|
ArgsUsage: "[state-root]",
|
||||||
|
Action: archiveGenerate,
|
||||||
|
Flags: slices.Concat([]cli.Flag{
|
||||||
|
archiveOutputFlag,
|
||||||
|
archiveCompactionIntervalFlag,
|
||||||
|
archiveDryRunFlag,
|
||||||
|
}, utils.NetworkFlags, utils.DatabaseFlags),
|
||||||
|
Description: `
|
||||||
|
Walks the state trie of the specified root (or head block) and archives
|
||||||
|
subtrees at height 3. Each archived subtree is replaced with an expiredNode
|
||||||
|
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.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Archive from head 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
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func archiveGenerate(ctx *cli.Context) error {
|
||||||
|
// 1. Setup node and databases
|
||||||
|
stack, _ := makeConfigNode(ctx)
|
||||||
|
defer stack.Close()
|
||||||
|
|
||||||
|
// Open database in write mode (readOnly=false) unless dry-run
|
||||||
|
dryRun := ctx.Bool(archiveDryRunFlag.Name)
|
||||||
|
chaindb := utils.MakeChainDatabase(ctx, stack, dryRun)
|
||||||
|
defer chaindb.Close()
|
||||||
|
|
||||||
|
// Check state scheme - we only support PathDB
|
||||||
|
scheme := cycleCheckScheme(ctx, chaindb)
|
||||||
|
if scheme != rawdb.PathScheme {
|
||||||
|
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) {
|
||||||
|
return errors.New("state trie not found in database")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Open archive writer (unless dry-run)
|
||||||
|
var writer *archive.ArchiveWriter
|
||||||
|
archivePath := ctx.String(archiveOutputFlag.Name)
|
||||||
|
if archivePath == "" {
|
||||||
|
archivePath = filepath.Join(stack.ResolvePath(""), "nodearchive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dryRun {
|
||||||
|
var err error
|
||||||
|
writer, err = archive.NewArchiveWriter(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open archive file %s: %w", archivePath, err)
|
||||||
|
}
|
||||||
|
defer writer.Close()
|
||||||
|
log.Info("Opened archive file", "path", archivePath)
|
||||||
|
} else {
|
||||||
|
log.Info("Dry run mode - no changes will be made")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create and run archiver
|
||||||
|
archiver := trie.NewArchiver(
|
||||||
|
chaindb,
|
||||||
|
triedb,
|
||||||
|
writer,
|
||||||
|
ctx.Uint64(archiveCompactionIntervalFlag.Name),
|
||||||
|
dryRun,
|
||||||
|
)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
if err := archiver.ProcessState(root); err != nil {
|
||||||
|
return fmt.Errorf("archive generation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Get stats and optionally run final compaction
|
||||||
|
subtrees, leaves, bytesDeleted := archiver.Stats()
|
||||||
|
|
||||||
|
if !dryRun && subtrees > 0 {
|
||||||
|
log.Info("Running final database compaction")
|
||||||
|
if err := chaindb.Compact(nil, nil); err != nil {
|
||||||
|
log.Warn("Final compaction failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Print summary
|
||||||
|
var archiveSize uint64
|
||||||
|
if writer != nil {
|
||||||
|
archiveSize = writer.Offset()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Archive generation complete",
|
||||||
|
"subtrees", subtrees,
|
||||||
|
"leaves", leaves,
|
||||||
|
"bytesDeleted", bytesDeleted,
|
||||||
|
"archiveSize", archiveSize,
|
||||||
|
"elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
log.Info("This was a dry run - no changes were made to the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return rawdb.ReadStateScheme(db)
|
||||||
|
}
|
||||||
|
|
@ -239,6 +239,8 @@ func init() {
|
||||||
dumpConfigCommand,
|
dumpConfigCommand,
|
||||||
// see dbcmd.go
|
// see dbcmd.go
|
||||||
dbCommand,
|
dbCommand,
|
||||||
|
// See archivecmd.go
|
||||||
|
archiveCommand,
|
||||||
// See cmd/utils/flags_legacy.go
|
// See cmd/utils/flags_legacy.go
|
||||||
utils.ShowDeprecated,
|
utils.ShowDeprecated,
|
||||||
// See snapshot.go
|
// See snapshot.go
|
||||||
|
|
|
||||||
92
trie/archive/writer.go
Normal file
92
trie/archive/writer.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArchiveWriter is an append-only writer for archive files.
|
||||||
|
// It writes RLP-encoded records to a file and tracks the current offset.
|
||||||
|
type ArchiveWriter struct {
|
||||||
|
file *os.File
|
||||||
|
offset uint64
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArchiveWriter creates a new archive writer that appends to the given file.
|
||||||
|
// If the file exists, it will be opened in append mode and writing continues
|
||||||
|
// from the current end of file. If it doesn't exist, it will be created.
|
||||||
|
func NewArchiveWriter(path string) (*ArchiveWriter, error) {
|
||||||
|
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ArchiveWriter{
|
||||||
|
file: file,
|
||||||
|
offset: uint64(info.Size()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteSubtree writes all records belonging to a subtree and returns
|
||||||
|
// the starting offset and total size of the written data.
|
||||||
|
// This is the atomic unit of archival - all records for a subtree are
|
||||||
|
// written together and can be retrieved together using the returned
|
||||||
|
// offset and size.
|
||||||
|
func (w *ArchiveWriter) WriteSubtree(records []*Record) (offset uint64, size uint64, err error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
startOffset := w.offset
|
||||||
|
for _, rec := range records {
|
||||||
|
encoded, err := rlp.EncodeToBytes(rec)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if _, err := w.file.Write(encoded); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
w.offset += uint64(len(encoded))
|
||||||
|
}
|
||||||
|
return startOffset, w.offset - startOffset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync flushes the file to disk. This should be called after writing
|
||||||
|
// a subtree and before modifying the database to ensure crash consistency.
|
||||||
|
func (w *ArchiveWriter) Sync() error {
|
||||||
|
return w.file.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the archive file.
|
||||||
|
func (w *ArchiveWriter) Close() error {
|
||||||
|
return w.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset returns the current write offset in the file.
|
||||||
|
func (w *ArchiveWriter) Offset() uint64 {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.offset
|
||||||
|
}
|
||||||
403
trie/archiver.go
Normal file
403
trie/archiver.go
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/archive"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// subtreeInfo holds information about a subtree to be archived.
|
||||||
|
// It contains all the data needed to write the subtree to an archive
|
||||||
|
// and replace it with an expiredNode in the database.
|
||||||
|
type subtreeInfo struct {
|
||||||
|
path []byte // Hex-encoded path to subtree root
|
||||||
|
owner common.Hash // Zero for account trie, account hash for storage
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archiver handles the archival process of trie nodes.
|
||||||
|
// It walks the state trie, identifies subtrees at height 3,
|
||||||
|
// archives their leaf data, and replaces them with expiredNode markers.
|
||||||
|
type Archiver struct {
|
||||||
|
db ethdb.Database
|
||||||
|
triedb database.NodeDatabase
|
||||||
|
writer *archive.ArchiveWriter
|
||||||
|
compactionInterval uint64
|
||||||
|
dryRun bool
|
||||||
|
stateRoot common.Hash
|
||||||
|
|
||||||
|
// Progress tracking
|
||||||
|
subtreesArchived uint64
|
||||||
|
bytesDeleted uint64
|
||||||
|
leavesArchived uint64
|
||||||
|
lastCompaction uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArchiver creates a new archiver instance.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - db: The underlying key-value database
|
||||||
|
// - triedb: The trie database for reading nodes
|
||||||
|
// - writer: Archive file writer (can be nil for dry run)
|
||||||
|
// - compactionInterval: Run compaction after this many subtrees (0 = disable)
|
||||||
|
// - dryRun: If true, don't modify the database
|
||||||
|
func NewArchiver(db ethdb.Database, triedb database.NodeDatabase,
|
||||||
|
writer *archive.ArchiveWriter, compactionInterval uint64, dryRun bool) *Archiver {
|
||||||
|
return &Archiver{
|
||||||
|
db: db,
|
||||||
|
triedb: triedb,
|
||||||
|
writer: writer,
|
||||||
|
compactionInterval: compactionInterval,
|
||||||
|
dryRun: dryRun,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessState archives subtrees from the given state root.
|
||||||
|
// It processes the account trie first, then all storage tries.
|
||||||
|
func (a *Archiver) ProcessState(root common.Hash) error {
|
||||||
|
a.stateRoot = root
|
||||||
|
|
||||||
|
// Process account trie (owner = zero hash)
|
||||||
|
log.Info("Processing account trie", "root", root)
|
||||||
|
accountTrie, err := New(StateTrieID(root), a.triedb)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open account trie: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.processTrie(common.Hash{}, accountTrie); err != nil {
|
||||||
|
return fmt.Errorf("failed to process account trie: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process storage tries for accounts with storage
|
||||||
|
log.Info("Processing storage tries")
|
||||||
|
iter, err := accountTrie.NodeIterator(nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create account iterator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kvIter := NewIterator(iter)
|
||||||
|
for kvIter.Next() {
|
||||||
|
// Decode the account to check for storage
|
||||||
|
var acc types.StateAccount
|
||||||
|
if err := rlp.DecodeBytes(kvIter.Value, &acc); err != nil {
|
||||||
|
log.Warn("Failed to decode account", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if acc.Root == types.EmptyRootHash {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process this account's storage trie
|
||||||
|
accountHash := common.BytesToHash(kvIter.Key)
|
||||||
|
storageID := StorageTrieID(root, accountHash, acc.Root)
|
||||||
|
storageTrie, err := New(storageID, a.triedb)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to open storage trie", "account", accountHash, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.processTrie(accountHash, storageTrie); err != nil {
|
||||||
|
log.Warn("Failed to process storage trie", "account", accountHash, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kvIter.Err != nil {
|
||||||
|
return fmt.Errorf("account iteration error: %w", kvIter.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processTrie finds and archives all height-3 subtrees in the trie.
|
||||||
|
func (a *Archiver) processTrie(owner common.Hash, t *Trie) error {
|
||||||
|
if t.root == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subtrees := a.findHeight3Subtrees(t.root, nil, owner)
|
||||||
|
log.Info("Found subtrees to archive", "owner", owner, "count", len(subtrees))
|
||||||
|
|
||||||
|
for _, info := range subtrees {
|
||||||
|
if err := a.archiveSubtree(info); err != nil {
|
||||||
|
log.Warn("Failed to archive subtree", "path", common.Bytes2Hex(info.path), "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.subtreesArchived++
|
||||||
|
a.leavesArchived += uint64(len(info.leaves))
|
||||||
|
|
||||||
|
if err := a.maybeCompact(); err != nil {
|
||||||
|
log.Warn("Compaction failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if info == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this subtree has height 3, it's a candidate for archival
|
||||||
|
if info.height == 3 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height < 3: no archivable subtrees here
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case valueNode:
|
||||||
|
// Leaf: height 0
|
||||||
|
// Encode the leaf as a shortNode for archive storage
|
||||||
|
return &subtreeInfo{
|
||||||
|
path: copyBytes(path),
|
||||||
|
owner: owner,
|
||||||
|
height: 0,
|
||||||
|
leaves: []*archive.Record{{
|
||||||
|
Path: nil, // Empty relative path for leaf at root
|
||||||
|
Value: []byte(n),
|
||||||
|
}},
|
||||||
|
nodePaths: [][]byte{copyBytes(path)},
|
||||||
|
}
|
||||||
|
|
||||||
|
case *shortNode:
|
||||||
|
childPath := append(append([]byte{}, path...), n.Key...)
|
||||||
|
childInfo := a.computeSubtreeInfo(n.Val, childPath, owner)
|
||||||
|
if childInfo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust relative paths in leaves to include this node's key
|
||||||
|
for _, leaf := range childInfo.leaves {
|
||||||
|
leaf.Path = append(append([]byte{}, n.Key...), leaf.Path...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &subtreeInfo{
|
||||||
|
path: copyBytes(path),
|
||||||
|
owner: owner,
|
||||||
|
height: childInfo.height + 1,
|
||||||
|
leaves: childInfo.leaves,
|
||||||
|
nodePaths: append([][]byte{copyBytes(path)}, childInfo.nodePaths...),
|
||||||
|
}
|
||||||
|
|
||||||
|
case *fullNode:
|
||||||
|
var (
|
||||||
|
maxHeight = 0
|
||||||
|
allLeaves []*archive.Record
|
||||||
|
allPaths = [][]byte{copyBytes(path)}
|
||||||
|
)
|
||||||
|
for i, child := range n.Children[:16] {
|
||||||
|
if child != nil {
|
||||||
|
childPath := append(append([]byte{}, path...), byte(i))
|
||||||
|
childInfo := a.computeSubtreeInfo(child, childPath, owner)
|
||||||
|
if childInfo != nil {
|
||||||
|
if childInfo.height+1 > maxHeight {
|
||||||
|
maxHeight = childInfo.height + 1
|
||||||
|
}
|
||||||
|
// Adjust relative paths to include the branch index
|
||||||
|
for _, leaf := range childInfo.leaves {
|
||||||
|
leaf.Path = append([]byte{byte(i)}, leaf.Path...)
|
||||||
|
}
|
||||||
|
allLeaves = append(allLeaves, childInfo.leaves...)
|
||||||
|
allPaths = append(allPaths, childInfo.nodePaths...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allLeaves) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &subtreeInfo{
|
||||||
|
path: copyBytes(path),
|
||||||
|
owner: owner,
|
||||||
|
height: maxHeight,
|
||||||
|
leaves: allLeaves,
|
||||||
|
nodePaths: allPaths,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 a.computeSubtreeInfo(resolved, path, owner)
|
||||||
|
|
||||||
|
case *expiredNode:
|
||||||
|
// Already archived, skip
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveSubtree writes leaves to archive and replaces subtree with expiredNode.
|
||||||
|
func (a *Archiver) archiveSubtree(info *subtreeInfo) error {
|
||||||
|
if a.dryRun {
|
||||||
|
log.Info("Would archive subtree",
|
||||||
|
"path", common.Bytes2Hex(info.path),
|
||||||
|
"owner", info.owner,
|
||||||
|
"height", info.height,
|
||||||
|
"leaves", len(info.leaves),
|
||||||
|
"nodes", len(info.nodePaths))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Write to archive file
|
||||||
|
offset, size, err := a.writer.WriteSubtree(info.leaves)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write subtree to archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sync to ensure durability before modifying DB
|
||||||
|
if err := a.writer.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Batch database operations
|
||||||
|
batch := a.db.NewBatch()
|
||||||
|
|
||||||
|
// Delete all nodes in subtree (except the root which we'll overwrite)
|
||||||
|
for _, nodePath := range info.nodePaths[1:] { // Skip first (root)
|
||||||
|
if info.owner == (common.Hash{}) {
|
||||||
|
rawdb.DeleteAccountTrieNode(batch, nodePath)
|
||||||
|
} else {
|
||||||
|
rawdb.DeleteStorageTrieNode(batch, info.owner, nodePath)
|
||||||
|
}
|
||||||
|
a.bytesDeleted += uint64(len(nodePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write expiredNode at subtree root
|
||||||
|
expiredBlob := encodeExpiredNodeBlob(offset, size)
|
||||||
|
if info.owner == (common.Hash{}) {
|
||||||
|
rawdb.WriteAccountTrieNode(batch, info.path, expiredBlob)
|
||||||
|
} else {
|
||||||
|
rawdb.WriteStorageTrieNode(batch, info.owner, info.path, expiredBlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
return fmt.Errorf("failed to write batch: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Archived subtree",
|
||||||
|
"path", common.Bytes2Hex(info.path),
|
||||||
|
"owner", info.owner,
|
||||||
|
"leaves", len(info.leaves),
|
||||||
|
"offset", offset,
|
||||||
|
"size", size)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeCompact runs database compaction if the threshold is reached.
|
||||||
|
func (a *Archiver) maybeCompact() error {
|
||||||
|
if a.compactionInterval == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if a.subtreesArchived-a.lastCompaction >= a.compactionInterval {
|
||||||
|
log.Info("Running database compaction", "subtrees", a.subtreesArchived)
|
||||||
|
if err := a.db.Compact(nil, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.lastCompaction = a.subtreesArchived
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveNode resolves a hashNode to its actual node content.
|
||||||
|
func (a *Archiver) resolveNode(hash hashNode, path []byte, owner common.Hash) (node, error) {
|
||||||
|
reader, err := a.triedb.NodeReader(a.stateRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blob, err := reader.Node(owner, path, common.BytesToHash(hash))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeNodeUnsafe(hash, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeExpiredNodeBlob creates the raw bytes for an expiredNode.
|
||||||
|
// Format: 1-byte marker (0x00) + 8-byte offset + 8-byte size = 17 bytes
|
||||||
|
func encodeExpiredNodeBlob(offset, size uint64) []byte {
|
||||||
|
buf := make([]byte, 1+2*archive.OffsetSize) // 17 bytes
|
||||||
|
buf[0] = expiredNodeMarker // 0x00
|
||||||
|
binary.BigEndian.PutUint64(buf[1:], offset)
|
||||||
|
binary.BigEndian.PutUint64(buf[1+archive.OffsetSize:], size)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns archival statistics.
|
||||||
|
func (a *Archiver) Stats() (subtrees, leaves, bytesDeleted uint64) {
|
||||||
|
return a.subtreesArchived, a.leavesArchived, a.bytesDeleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyBytes returns a copy of the given byte slice.
|
||||||
|
func copyBytes(b []byte) []byte {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := make([]byte, len(b))
|
||||||
|
copy(c, b)
|
||||||
|
return c
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue