mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
trie: fix nodeHeight over-estimate so height-3 branches are archived (#577)
The nodeHeight *fullNode case returned maxHeight+1 once the running max height reached maxHeight via one child: the `if maxH+1 > maxHeight` guard fired on the next hashNode child, reporting a genuine height-3 branch as height 4. With the `height == 3` archival predicate this made dense, branch-heavy tries (notably the account trie, whose height-3 nodes are all multi-child branches) archive nothing, while only sparse, extension-heavy storage tries archived anything. On a jochemnet shadowfork (path scheme, ~379 GB live KV) a 15.5h `archive generate` archived only 122,340 subtrees / 16.6 MB, and the account trie archived nothing (count=0), with zero read/collection failures in the log -- confirming a height computation bug rather than a read-path issue. Replace the running-max guard with a depth-budget guard mirroring the already-correct *shortNode case; keep the post-update `maxH > maxHeight` exceeded check so recursion and raw-DB reads stay bounded. Add trie/archiver_test.go: a height-3 root branch probes as height 3 (it returned 4 before this change). Co-authored-by: CPerezz <claude.ai.monorail916@passmail.net>
This commit is contained in:
parent
3e6e4f17a0
commit
69b330d98e
2 changed files with 86 additions and 3 deletions
|
|
@ -283,6 +283,11 @@ func (a *Archiver) nodeHeight(n node, path []byte, owner common.Hash, maxHeight
|
|||
}
|
||||
|
||||
case *fullNode:
|
||||
if maxHeight <= 0 {
|
||||
// No depth budget left: a fullNode always has at least one
|
||||
// child, so its height is >= 1, i.e. already > maxHeight.
|
||||
return maxHeight + 1
|
||||
}
|
||||
maxH := 0
|
||||
for i, child := range n.Children[:16] {
|
||||
if child == nil {
|
||||
|
|
@ -294,9 +299,6 @@ func (a *Archiver) nodeHeight(n node, path []byte, owner common.Hash, maxHeight
|
|||
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)
|
||||
|
|
|
|||
81
trie/archiver_test.go
Normal file
81
trie/archiver_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// 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 (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
)
|
||||
|
||||
// TestProbeHeightFullNodeBranch is a regression test for an over-estimate bug in
|
||||
// nodeHeight's *fullNode case. The previous guard
|
||||
//
|
||||
// if maxH+1 > maxHeight { return maxHeight + 1 }
|
||||
//
|
||||
// fired once the running max height reached maxHeight via one child, so the next
|
||||
// hashNode child made a genuine height-3 branch report as height 4. Combined with
|
||||
// the "archive only height == 3" predicate, dense branch-heavy tries (notably the
|
||||
// account trie, whose height-3 nodes are all multi-child branches) archived
|
||||
// nothing. Pre-fix probeHeight returns 4 here; post-fix it must return 3.
|
||||
//
|
||||
// The trie built below has a height-3 ROOT branch:
|
||||
//
|
||||
// root (branch @ nibble0, children at 0 and 1) height 3
|
||||
// ├─ child0 (branch @ nibble1) ├─ leafA ├─ leafB height 2
|
||||
// └─ child1 (branch @ nibble1) ├─ leafC ├─ leafD height 2
|
||||
//
|
||||
// Values are 40 bytes so leaves are NOT embedded; root's children are hashNodes,
|
||||
// which is exactly what exercises the buggy branch of nodeHeight.
|
||||
func TestProbeHeightFullNodeBranch(t *testing.T) {
|
||||
big := func(b byte) []byte { return bytes.Repeat([]byte{b}, 40) }
|
||||
|
||||
tr := NewEmpty(nil)
|
||||
keys := [][]byte{
|
||||
{0x00, 0x11, 0x11, 0x11}, // nibbles 0,0,...
|
||||
{0x01, 0x22, 0x22, 0x22}, // nibbles 0,1,...
|
||||
{0x10, 0x33, 0x33, 0x33}, // nibbles 1,0,...
|
||||
{0x11, 0x44, 0x44, 0x44}, // nibbles 1,1,...
|
||||
}
|
||||
for i, k := range keys {
|
||||
if err := tr.Update(k, big(byte('A'+i))); err != nil {
|
||||
t.Fatalf("update %x: %v", k, err)
|
||||
}
|
||||
}
|
||||
root, nodes := tr.Commit(false)
|
||||
if nodes == nil {
|
||||
t.Fatal("nil node set after commit")
|
||||
}
|
||||
|
||||
// Persist the committed nodes under account-trie path keys so the archiver's
|
||||
// raw-DB reader (readNodeBlob -> ReadAccountTrieNode) can resolve them.
|
||||
raw := rawdb.NewMemoryDatabase()
|
||||
for path, n := range nodes.Nodes {
|
||||
if n == nil || len(n.Blob) == 0 { // skip deletions
|
||||
continue
|
||||
}
|
||||
rawdb.WriteAccountTrieNode(raw, []byte(path), n.Blob)
|
||||
}
|
||||
a := &Archiver{db: raw}
|
||||
|
||||
if got := a.probeHeight(common.Hash{}, nil, root, 3); got != 3 {
|
||||
t.Fatalf("probeHeight(height-3 root branch) = %d, want 3 "+
|
||||
"(a height-3 branch must be detected as height 3, not over-estimated)", got)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue