triedb/pathdb: skip duplicate-root layer insertion (#34642)

PathDB keys diff layers by state root, not by block hash. That means a
side-chain block can legitimately collide with an existing canonical diff layer
when both blocks produce the same post-state (for example same parent, 
same coinbase, no txs).

Today `layerTree.add` blindly inserts that second layer. If the root
already exists, this overwrites `tree.layers[root]` and appends the same 
root to the mutation lookup again. Later account/storage lookups resolve 
that root to the wrong diff layer, which can corrupt reads for descendant 
canonical states.

At runtime, the corruption is silent: no error is logged and no invariant check
fires. State reads against affected descendants simply return stale data
from the wrong diff layer (for example, an account balance that reflects one
fewer block reward), which can propagate into RPC responses and block 
validation.

This change makes duplicate-root inserts idempotent. A second layer with
the same state root does not add any new retrievable state to a tree that is
already keyed by root; keeping the original layer preserves the existing parent 
chain and avoids polluting the lookup history with duplicate roots.

The regression test imports a canonical chain of two layers followed by
a fork layer at height 1 with the same state root but a different block hash. 
Before the fix, account and storage lookups at the head resolve the fork 
layer instead of the canonical one. After the fix, the duplicate insert is 
skipped and lookups remain correct.
This commit is contained in:
Diego López León 2026-04-07 10:31:41 -03:00 committed by GitHub
parent b5d322000c
commit 52b8c09fdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 0 deletions

View file

@ -151,6 +151,15 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
if root == parentRoot {
return errors.New("layer cycle")
}
// If a layer with this root already exists, skip the insertion. Fork blocks
// can produce the same state root as the canonical block (same parent, same
// coinbase, zero txs); overwriting tree.layers[root] would corrupt the parent
// chain for any child layers already built on top of the existing one, and
// appending a duplicate root to the lookup indices causes accountTip/storageTip
// to resolve the wrong layer.
if tree.get(root) != nil {
return nil
}
parent := tree.get(parentRoot)
if parent == nil {
return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot)

View file

@ -575,6 +575,40 @@ func TestDescendant(t *testing.T) {
}
}
func TestDuplicateRootLookup(t *testing.T) {
// Chain:
// C1->C2->C3 (HEAD)
tr := newTestLayerTree() // base = 0x1
tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
// A fork block with the same state root as C2; inserting it must not
// pollute the lookup history for the canonical descendant C3.
tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
if n := tr.len(); n != 3 {
t.Fatalf("duplicate root insert changed layer count, got %d, want 3", n)
}
l, err := tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3})
if err != nil {
t.Fatalf("account lookup failed: %v", err)
}
if l.rootHash() != (common.Hash{0x3}) {
t.Errorf("unexpected account tip, want %x, got %x", common.Hash{0x3}, l.rootHash())
}
l, err = tr.lookupStorage(common.HexToHash("0xa"), common.HexToHash("0x1"), common.Hash{0x3})
if err != nil {
t.Fatalf("storage lookup failed: %v", err)
}
if l.rootHash() != (common.Hash{0x3}) {
t.Errorf("unexpected storage tip, want %x, got %x", common.Hash{0x3}, l.rootHash())
}
}
func TestAccountLookup(t *testing.T) {
// Chain:
// C1->C2->C3->C4 (HEAD)