diff --git a/cmd/geth/archivecmd.go b/cmd/geth/archivecmd.go index a2c6f41b5a..d6e241974e 100644 --- a/cmd/geth/archivecmd.go +++ b/cmd/geth/archivecmd.go @@ -197,7 +197,6 @@ func archiveGenerate(ctx *cli.Context) error { 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() @@ -208,27 +207,39 @@ func archiveGenerate(ctx *cli.Context) error { return fmt.Errorf("archive generation requires path-based state scheme, got: %s", scheme) } - // 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. + // 2. Flush diff layers to disk via pathdb. This ensures the raw DB + // contains the complete, up-to-date state trie and that state history + // entries are properly written to the freezer. + trieDB := utils.MakeTrieDatabase(ctx, stack, chaindb, false, dryRun, false) + head, hasDiff := trieDB.DiffHead() + if hasDiff { + log.Info("Flushing diff layers to disk", "head", head) + if err := trieDB.Commit(head, true); err != nil { + trieDB.Close() + return fmt.Errorf("failed to flush diff layers: %w", err) + } + log.Info("Diff layers flushed successfully") + } else { + log.Info("No diff layers to flush, disk state is current", "root", head) + } + // Close triedb — we work directly with raw DB for archival. + // We'll re-open it at the end to write a fresh journal. + trieDB.Close() + + // 3. Determine the disk state root (now up-to-date after flush). 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) + log.Info("Using 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) + // 4. Open archive writer (unless dry-run). + // The archive file is placed at /geth/nodearchive by default, + // matching the path used by ArchivedNodeResolver when reading back. var writer *archive.ArchiveWriter archivePath := ctx.String(archiveOutputFlag.Name) if archivePath == "" { @@ -247,7 +258,7 @@ func archiveGenerate(ctx *cli.Context) error { log.Info("Dry run mode - no changes will be made") } - // 4. Create and run archiver + // 5. Create and run archiver archiver := trie.NewArchiver( chaindb, nodeDB, @@ -261,7 +272,7 @@ func archiveGenerate(ctx *cli.Context) error { return fmt.Errorf("archive generation failed: %w", err) } - // 5. Get stats and optionally run final compaction + // 6. Get stats and optionally run final compaction subtrees, leaves, bytesDeleted := archiver.Stats() if !dryRun && subtrees > 0 { @@ -271,28 +282,22 @@ func archiveGenerate(ctx *cli.Context) error { } } + // 7. Re-journal the pathdb state with the current disk root. + // After archiving, some trie nodes have been replaced with expired + // markers. We re-open pathdb and write a fresh journal (disk layer + // only, since all diff layers were flushed in step 2) so that geth + // can restart cleanly. 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) - } - } + log.Info("Re-journaling pathdb state") + freshTrieDB := utils.MakeTrieDatabase(ctx, stack, chaindb, false, false, false) + freshRoot := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(chaindb, nil)) + if err := freshTrieDB.Journal(freshRoot); err != nil { + log.Warn("Failed to re-journal pathdb state", "err", err) } + freshTrieDB.Close() } - // 6. Print summary + // 8. Print summary var archiveSize uint64 if writer != nil { archiveSize = writer.Offset() diff --git a/trie/archiver.go b/trie/archiver.go index 817257c087..32b2eae711 100644 --- a/trie/archiver.go +++ b/trie/archiver.go @@ -46,6 +46,12 @@ type subtreeInfo struct { // 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. +// +// The archiver uses a streaming approach: it walks the trie using a +// NodeIterator, probes each node's height via bounded raw DB reads, +// and archives subtrees immediately when found. This keeps memory +// usage proportional to the iterator stack depth + the current subtree +// being processed, rather than loading the entire trie into memory. type Archiver struct { db ethdb.Database triedb database.NodeDatabase @@ -134,23 +140,69 @@ func (a *Archiver) ProcessState(root common.Hash) error { return nil } -// processTrie finds and archives all height-3 subtrees in the trie. +// processTrie finds and archives all height-3 subtrees in the trie using +// a streaming approach. It walks the trie with a NodeIterator, probes each +// node's height via bounded raw DB reads, and archives subtrees immediately. +// +// Memory usage is O(iterator_stack_depth + current_subtree_size) instead of +// O(entire_trie) as with the previous recursive approach. 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)) + iter, err := t.NodeIterator(nil) + if err != nil { + return fmt.Errorf("failed to create node iterator: %w", err) + } - lastLog := time.Now() - for i, info := range subtrees { + var ( + lastLog = time.Now() + found uint64 + ) + + for iter.Next(true) { + if iter.Leaf() { + continue + } + + // Progress logging if time.Since(lastLog) > 30*time.Second { - log.Info("Archiving subtrees", "owner", owner, "progress", fmt.Sprintf("%d/%d", i, len(subtrees)), "archived", a.subtreesArchived) + log.Info("Scanning trie for subtrees", + "owner", owner, + "path", common.Bytes2Hex(iter.Path()), + "found", found, + "archived", a.subtreesArchived) lastLog = time.Now() } + + path := copyBytes(iter.Path()) + hash := iter.Hash() + if hash == (common.Hash{}) { + // Embedded node (no hash), skip — it will be part of a + // parent subtree. + continue + } + + // Probe subtree height via bounded raw DB reads. + // This does NOT load the trie into memory — it reads blobs from + // the DB, decodes them, computes height, and discards them. + height := a.probeHeight(owner, path, hash, 3) + if height != 3 { + // Too small to archive; the iterator will visit children. + // Too tall — descend into children to find height-3 subtrees. + continue + } + + // height == 3: collect and archive this subtree immediately. + info := a.collectSubtree(owner, path, hash) + if info == nil { + continue + } + found++ + if err := a.archiveSubtree(info); err != nil { - log.Warn("Failed to archive subtree", "path", common.Bytes2Hex(info.path), "err", err) + log.Warn("Failed to archive subtree", "path", common.Bytes2Hex(path), "err", err) continue } a.subtreesArchived++ @@ -159,178 +211,275 @@ func (a *Archiver) processTrie(owner common.Hash, t *Trie) error { if err := a.maybeCompact(); err != nil { log.Warn("Compaction failed", "err", err) } + + // Skip children — they're now archived. + // We call Next(false) to move past the subtree without descending. + iter.Next(false) } + + if iter.Error() != nil { + return fmt.Errorf("iterator error: %w", iter.Error()) + } + + log.Info("Found subtrees to archive", "owner", owner, "count", found) return nil } -// findHeight3Subtrees recursively finds all subtrees with height == 3. +// probeHeight computes the height of a node by reading from the raw DB. +// It stops early once height exceeds maxHeight (returns maxHeight+1). +// The decoded nodes are not retained — they are discarded after inspection. +// // Height is measured from leaves: leaves=0, their parents=1, etc. -func (a *Archiver) findHeight3Subtrees(n node, path []byte, owner common.Hash) []*subtreeInfo { - 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) +func (a *Archiver) probeHeight(owner common.Hash, path []byte, hash common.Hash, maxHeight int) int { + blob := a.readNodeBlob(owner, path) + if len(blob) == 0 { + return 0 } - if info == nil { + + // Already expired — skip. + if blob[0] == expiredNodeMarker { + return -1 + } + + n, err := decodeNodeUnsafe(hash[:], blob) + if err != nil { + return 0 + } + + return a.nodeHeight(n, path, owner, maxHeight) +} + +// nodeHeight computes the height of a decoded node, bounded by maxHeight. +// Returns maxHeight+1 early if the subtree is taller than maxHeight. +func (a *Archiver) nodeHeight(n node, path []byte, owner common.Hash, maxHeight int) int { + switch n := n.(type) { + case nil: + return 0 + + case valueNode: + return 0 + + case *shortNode: + childPath := append(append([]byte{}, path...), n.Key...) + switch child := n.Val.(type) { + case valueNode: + return 1 // shortNode → leaf + case hashNode: + if maxHeight <= 1 { + return maxHeight + 1 + } + childHeight := a.probeHeight(owner, childPath, common.BytesToHash(child), maxHeight-1) + if childHeight < 0 { + return -1 // expired child + } + return childHeight + 1 + default: + // Inline node + childHeight := a.nodeHeight(child, childPath, owner, maxHeight-1) + if childHeight < 0 { + return -1 + } + return childHeight + 1 + } + + case *fullNode: + maxH := 0 + for i, child := range n.Children[:16] { + if child == nil { + continue + } + childPath := append(append([]byte{}, path...), byte(i)) + var childHeight int + switch c := child.(type) { + case valueNode: + childHeight = 0 + case hashNode: + if maxH+1 > maxHeight { + return maxHeight + 1 + } + childHeight = a.probeHeight(owner, childPath, common.BytesToHash(c), maxHeight-1) + default: + childHeight = a.nodeHeight(c, childPath, owner, maxHeight-1) + } + if childHeight < 0 { + continue // expired child, skip + } + h := childHeight + 1 + if h > maxH { + maxH = h + } + if maxH > maxHeight { + return maxHeight + 1 + } + } + return maxH + + case hashNode: + return a.probeHeight(owner, path, common.BytesToHash(n), maxHeight) + + case *expiredNode: + return -1 + } + return 0 +} + +// collectSubtree reads a height-3 subtree from the raw DB and collects its +// leaves and node paths for archival. The subtree is bounded (height ≤ 3), +// so memory usage is limited. +func (a *Archiver) collectSubtree(owner common.Hash, path []byte, hash common.Hash) *subtreeInfo { + blob := a.readNodeBlob(owner, path) + if len(blob) == 0 { + return nil + } + if blob[0] == expiredNodeMarker { 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} + n, err := decodeNodeUnsafe(hash[:], blob) + if err != nil { + log.Warn("Failed to decode node for collection", "path", common.Bytes2Hex(path), "err", err) + return nil } - // If height > 3, recurse into children to find height-3 subtrees - if info.height > 3 { - return a.findSubtreesInChildren(n, path, owner) + info := &subtreeInfo{ + path: copyBytes(path), + owner: owner, + rootHash: hash, } - // Height < 3: no archivable subtrees here - return nil + leaves, nodePaths, height, err := a.collectNodeLeaves(n, path, nil, owner) + if err != nil { + log.Warn("Failed to collect subtree leaves", "path", common.Bytes2Hex(path), "err", err) + return nil + } + + info.height = height + info.leaves = leaves + info.nodePaths = append([][]byte{copyBytes(path)}, nodePaths...) + return info } -// 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, 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) { +// collectNodeLeaves recursively collects all leaves and node paths in a +// bounded subtree. relPath is the path relative to the subtree root. +// Returns (leaves, nodePaths, height, error). +func (a *Archiver) collectNodeLeaves(n node, absPath, relPath []byte, owner common.Hash) ([]*archive.Record, [][]byte, int, error) { switch n := n.(type) { case nil: - return nil, nil + return nil, nil, 0, nil case valueNode: - // Leaf: height 0 - 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)}, - }, nil + return []*archive.Record{{ + Path: copyBytes(relPath), + Value: []byte(n), + }}, nil, 0, nil case *shortNode: - childPath := append(append([]byte{}, path...), n.Key...) - childInfo, err := a.computeSubtreeInfo(n.Val, childPath, owner) + childAbsPath := append(append([]byte{}, absPath...), n.Key...) + var childNode node + switch c := n.Val.(type) { + case hashNode: + resolved, err := a.resolveRawNode(owner, childAbsPath, common.BytesToHash(c)) + if err != nil { + return nil, nil, 0, fmt.Errorf("resolve shortNode child at %s: %w", common.Bytes2Hex(childAbsPath), err) + } + childNode = resolved + default: + childNode = c + } + + // Pass nil relPath to child — we prepend the key ourselves + leaves, nodePaths, height, err := a.collectNodeLeaves(childNode, childAbsPath, nil, owner) if err != nil { - return nil, fmt.Errorf("shortNode key=%x: %w", n.Key, err) - } - if childInfo == nil { - return nil, nil + return nil, nil, 0, err } - // 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...) + // Prepend [relPath + extension key] to leaf relative paths + prefix := append(append([]byte{}, relPath...), n.Key...) + for _, leaf := range leaves { + leaf.Path = append(append([]byte{}, prefix...), leaf.Path...) } - return &subtreeInfo{ - path: copyBytes(path), - owner: owner, - height: childInfo.height + 1, - leaves: childInfo.leaves, - nodePaths: append([][]byte{copyBytes(path)}, childInfo.nodePaths...), - }, nil + return leaves, append([][]byte{copyBytes(absPath)}, nodePaths...), height + 1, nil case *fullNode: var ( - maxHeight = 0 allLeaves []*archive.Record - allPaths = [][]byte{copyBytes(path)} + allPaths [][]byte + maxHeight int ) for i, child := range n.Children[:16] { - if child != nil { - childPath := append(append([]byte{}, path...), byte(i)) - childInfo, err := a.computeSubtreeInfo(child, childPath, owner) + if child == nil { + continue + } + childAbsPath := append(append([]byte{}, absPath...), byte(i)) + + var childNode node + switch c := child.(type) { + case hashNode: + resolved, err := a.resolveRawNode(owner, childAbsPath, common.BytesToHash(c)) 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 - } - // 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...) + return nil, nil, 0, fmt.Errorf("resolve fullNode child[%x] at %s: %w", i, common.Bytes2Hex(childAbsPath), err) } + childNode = resolved + default: + childNode = c + } + + // Pass nil relPath to child — we prepend the index ourselves + leaves, nodePaths, height, err := a.collectNodeLeaves(childNode, childAbsPath, nil, owner) + if err != nil { + return nil, nil, 0, err + } + + // Prepend [relPath + branch index] to leaf relative paths + prefix := append(append([]byte{}, relPath...), byte(i)) + for _, leaf := range leaves { + leaf.Path = append(append([]byte{}, prefix...), leaf.Path...) + } + + allLeaves = append(allLeaves, leaves...) + allPaths = append(allPaths, nodePaths...) + h := height + 1 + if h > maxHeight { + maxHeight = h } } - - if len(allLeaves) == 0 { - return nil, nil - } - - return &subtreeInfo{ - path: copyBytes(path), - owner: owner, - height: maxHeight, - leaves: allLeaves, - nodePaths: allPaths, - }, nil + return allLeaves, allPaths, maxHeight, nil case hashNode: - resolved, err := a.resolveNode(n, path, owner) + resolved, err := a.resolveRawNode(owner, absPath, common.BytesToHash(n)) if err != nil { - return nil, fmt.Errorf("failed to resolve hashNode at path %s: %w", common.Bytes2Hex(path), err) + return nil, nil, 0, err } - return a.computeSubtreeInfo(resolved, path, owner) + return a.collectNodeLeaves(resolved, absPath, relPath, owner) case *expiredNode: - // Already archived, skip - return nil, nil + return nil, nil, 0, nil } - return nil, nil + return nil, nil, 0, nil +} + +// readNodeBlob reads a trie node blob directly from the raw key-value +// database, bypassing pathdb layers. +func (a *Archiver) readNodeBlob(owner common.Hash, path []byte) []byte { + if owner == (common.Hash{}) { + return rawdb.ReadAccountTrieNode(a.db, path) + } + return rawdb.ReadStorageTrieNode(a.db, owner, path) +} + +// resolveRawNode reads and decodes a trie node directly from the raw DB. +// Unlike resolveNode, this does NOT use the trie database (no caching, +// no diff layers). The decoded node is ephemeral and will be GC'd after use. +func (a *Archiver) resolveRawNode(owner common.Hash, path []byte, hash common.Hash) (node, error) { + blob := a.readNodeBlob(owner, path) + if len(blob) == 0 { + return nil, fmt.Errorf("node not found: owner=%s path=%s", owner, common.Bytes2Hex(path)) + } + if blob[0] == expiredNodeMarker { + return &expiredNode{}, nil + } + return decodeNodeUnsafe(hash[:], blob) } // archiveSubtree writes leaves to archive and replaces subtree with expiredNode. @@ -424,19 +573,6 @@ func (a *Archiver) maybeCompact() error { 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 { diff --git a/trie/expired_node.go b/trie/expired_node.go index 9a93f137c8..ce622daa03 100644 --- a/trie/expired_node.go +++ b/trie/expired_node.go @@ -20,7 +20,9 @@ import ( "bytes" "encoding/binary" "fmt" + "time" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/archive" ) @@ -68,6 +70,7 @@ func (n *expiredNode) SetArchiveResolver(resolver archive.ResolverFn) { // 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) { + start := time.Now() records, err := archive.ArchivedNodeResolver(n.offset, n.size) if err != nil { return nil, fmt.Errorf("failed to resolve expired node: %w", err) @@ -76,6 +79,11 @@ func resolveExpiredNodeData(n *expiredNode) (node, error) { if err != nil { return nil, fmt.Errorf("failed to rebuild expired node from archive: %w", err) } + depth := subtreeDepth(resolved) + log.Debug("Resurrected expired node from archive", + "offset", n.offset, "archiveBytes", n.size, + "records", len(records), "depth", depth, + "elapsed", time.Since(start)) // 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 @@ -113,6 +121,26 @@ func resolveExpiredNodeData(n *expiredNode) (node, error) { return resolved, nil } +// subtreeDepth returns the maximum depth of a trie subtree. +func subtreeDepth(n node) int { + switch n := n.(type) { + case *fullNode: + max := 0 + for _, child := range &n.Children { + if child != nil { + if d := subtreeDepth(child); d > max { + max = d + } + } + } + return 1 + max + case *shortNode: + return 1 + subtreeDepth(n.Val) + default: + return 0 + } +} + // 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. diff --git a/trie/trie.go b/trie/trie.go index d8282a4e2d..69db68b515 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -503,7 +503,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error return true, nn, nil case *expiredNode: - log.Info("Resolving expired node in insert()", "owner", t.owner, "offset", n.offset, "size", n.size) + log.Debug("Resolving expired node in insert()", "owner", t.owner, "offset", n.offset, "size", n.size) rn, err := resolveExpiredNodeData(n) if err != nil { return false, nil, err @@ -676,7 +676,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { return true, nn, nil case *expiredNode: - log.Info("Resolving expired node in delete()", "owner", t.owner, "offset", n.offset, "size", n.size) + log.Debug("Resolving expired node in delete()", "owner", t.owner, "offset", n.offset, "size", n.size) rn, err := resolveExpiredNodeData(n) if err != nil { return false, nil, err