triedb/pathdb: skip duplicate-root layer insertion

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

layerTree.add blindly overwrites tree.layers[root] in this case,
corrupting the parent chain for any child layers already built on top of
the existing one and appending a duplicate root to the lookup indices so
that accountTip/storageTip resolve the wrong diff layer.

Make duplicate-root inserts idempotent: a second layer with an already-
present state root carries no new retrievable state, so keeping the
original layer preserves the existing parent chain and avoids polluting
the lookup history.
This commit is contained in:
Diego López León 2026-04-02 19:39:33 -03:00
parent bcb0efd756
commit f74037c010
No known key found for this signature in database
GPG key ID: 1688910EC6300A60
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)