Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 64cabd1

Browse files
rjl493456442fjl
authored andcommitted
trie: remove inconsistent trie nodes during sync in path mode (#28595)
This fixes a database corruption issue that could occur during state healing. When sync is aborted while certain modifications were already committed, and a reorg occurs, the database would contain incorrect trie nodes stored by path. These nodes need to detected/deleted in order to obtain a complete and fully correct state after state healing. --------- Co-authored-by: Felix Lange <[email protected]>
1 parent d97cf83 commit 64cabd1

File tree

3 files changed

+255
-68
lines changed

3 files changed

+255
-68
lines changed

ethdb/dbtest/testsuite.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,13 @@ func TestDatabaseSuite(t *testing.T, New func() ethdb.KeyValueStore) {
272272
b.Put([]byte("5"), nil)
273273
b.Delete([]byte("1"))
274274
b.Put([]byte("6"), nil)
275-
b.Delete([]byte("3"))
275+
276+
b.Delete([]byte("3")) // delete then put
276277
b.Put([]byte("3"), nil)
277278

279+
b.Put([]byte("7"), nil) // put then delete
280+
b.Delete([]byte("7"))
281+
278282
if err := b.Write(); err != nil {
279283
t.Fatal(err)
280284
}

trie/sync.go

Lines changed: 128 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package trie
1919
import (
2020
"errors"
2121
"fmt"
22+
"sync"
2223

2324
"github.com/ethereum/go-ethereum/common"
2425
"github.com/ethereum/go-ethereum/common/prque"
@@ -98,10 +99,9 @@ func NewSyncPath(path []byte) SyncPath {
9899

99100
// nodeRequest represents a scheduled or already in-flight trie node retrieval request.
100101
type nodeRequest struct {
101-
hash common.Hash // Hash of the trie node to retrieve
102-
path []byte // Merkle path leading to this node for prioritization
103-
data []byte // Data content of the node, cached until all subtrees complete
104-
deletes [][]byte // List of internal path segments for trie nodes to delete
102+
hash common.Hash // Hash of the trie node to retrieve
103+
path []byte // Merkle path leading to this node for prioritization
104+
data []byte // Data content of the node, cached until all subtrees complete
105105

106106
parent *nodeRequest // Parent state node referencing this entry
107107
deps int // Number of dependencies before allowed to commit this node
@@ -128,37 +128,69 @@ type CodeSyncResult struct {
128128
Data []byte // Data content of the retrieved bytecode
129129
}
130130

131+
// nodeOp represents an operation upon the trie node. It can either represent a
132+
// deletion to the specific node or a node write for persisting retrieved node.
133+
type nodeOp struct {
134+
owner common.Hash // identifier of the trie (empty for account trie)
135+
path []byte // path from the root to the specified node.
136+
blob []byte // the content of the node (nil for deletion)
137+
hash common.Hash // hash of the node content (empty for node deletion)
138+
}
139+
140+
// isDelete indicates if the operation is a database deletion.
141+
func (op *nodeOp) isDelete() bool {
142+
return len(op.blob) == 0
143+
}
144+
131145
// syncMemBatch is an in-memory buffer of successfully downloaded but not yet
132146
// persisted data items.
133147
type syncMemBatch struct {
134-
nodes map[string][]byte // In-memory membatch of recently completed nodes
135-
hashes map[string]common.Hash // Hashes of recently completed nodes
136-
deletes map[string]struct{} // List of paths for trie node to delete
137-
codes map[common.Hash][]byte // In-memory membatch of recently completed codes
148+
scheme string // State scheme identifier
149+
nodes []nodeOp // In-memory batch of recently completed/deleted nodes
150+
codes map[common.Hash][]byte // In-memory membatch of recently completed codes
138151
}
139152

140153
// newSyncMemBatch allocates a new memory-buffer for not-yet persisted trie nodes.
141-
func newSyncMemBatch() *syncMemBatch {
154+
func newSyncMemBatch(scheme string) *syncMemBatch {
142155
return &syncMemBatch{
143-
nodes: make(map[string][]byte),
144-
hashes: make(map[string]common.Hash),
145-
deletes: make(map[string]struct{}),
146-
codes: make(map[common.Hash][]byte),
156+
scheme: scheme,
157+
codes: make(map[common.Hash][]byte),
147158
}
148159
}
149160

150-
// hasNode reports the trie node with specific path is already cached.
151-
func (batch *syncMemBatch) hasNode(path []byte) bool {
152-
_, ok := batch.nodes[string(path)]
153-
return ok
154-
}
155-
156161
// hasCode reports the contract code with specific hash is already cached.
157162
func (batch *syncMemBatch) hasCode(hash common.Hash) bool {
158163
_, ok := batch.codes[hash]
159164
return ok
160165
}
161166

167+
// addCode caches a contract code database write operation.
168+
func (batch *syncMemBatch) addCode(hash common.Hash, code []byte) {
169+
batch.codes[hash] = code
170+
}
171+
172+
// addNode caches a node database write operation.
173+
func (batch *syncMemBatch) addNode(owner common.Hash, path []byte, blob []byte, hash common.Hash) {
174+
batch.nodes = append(batch.nodes, nodeOp{
175+
owner: owner,
176+
path: path,
177+
blob: blob,
178+
hash: hash,
179+
})
180+
}
181+
182+
// delNode caches a node database delete operation.
183+
func (batch *syncMemBatch) delNode(owner common.Hash, path []byte) {
184+
if batch.scheme != rawdb.PathScheme {
185+
log.Error("Unexpected node deletion", "owner", owner, "path", path, "scheme", batch.scheme)
186+
return // deletion is not supported in hash mode.
187+
}
188+
batch.nodes = append(batch.nodes, nodeOp{
189+
owner: owner,
190+
path: path,
191+
})
192+
}
193+
162194
// Sync is the main state trie synchronisation scheduler, which provides yet
163195
// unknown trie hashes to retrieve, accepts node data associated with said hashes
164196
// and reconstructs the trie step by step until all is done.
@@ -194,7 +226,7 @@ func NewSync(root common.Hash, database ethdb.KeyValueReader, callback LeafCallb
194226
ts := &Sync{
195227
scheme: scheme,
196228
database: database,
197-
membatch: newSyncMemBatch(),
229+
membatch: newSyncMemBatch(scheme),
198230
nodeReqs: make(map[string]*nodeRequest),
199231
codeReqs: make(map[common.Hash]*codeRequest),
200232
queue: prque.New(nil),
@@ -213,16 +245,18 @@ func (s *Sync) AddSubTrie(root common.Hash, path []byte, parent common.Hash, par
213245
if root == emptyRoot {
214246
return
215247
}
216-
if s.membatch.hasNode(path) {
217-
return
218-
}
219248
if s.bloom == nil || s.bloom.Contains(root[:]) {
220249
// Bloom filter says this might be a duplicate, double check.
221250
// If database says yes, then at least the trie node is present
222251
// and we hold the assumption that it's NOT legacy contract code.
223252
owner, inner := ResolvePath(path)
224-
if rawdb.HasTrieNode(s.database, owner, inner, root, s.scheme) {
253+
exist, inconsistent := s.hasNode(owner, inner, root)
254+
if exist {
255+
// The entire subtrie is already present in the database.
225256
return
257+
} else if inconsistent {
258+
// There is a pre-existing node with the wrong hash in DB, remove it.
259+
s.membatch.delNode(owner, inner)
226260
}
227261
// False positive, bump fault meter
228262
bloomFaultMeter.Mark(1)
@@ -382,36 +416,39 @@ func (s *Sync) ProcessNode(result NodeSyncResult) error {
382416
}
383417

384418
// Commit flushes the data stored in the internal membatch out to persistent
385-
// storage, returning any occurred error.
419+
// storage, returning any occurred error. The whole data set will be flushed
420+
// in an atomic database batch.
386421
func (s *Sync) Commit(dbw ethdb.Batch) error {
387-
// Flush the pending node writes into database batch.
388422
var (
389423
account int
390424
storage int
391425
)
392-
for path, value := range s.membatch.nodes {
393-
owner, inner := ResolvePath([]byte(path))
394-
if owner == (common.Hash{}) {
395-
account += 1
426+
// Flush the pending node writes into database batch.
427+
for _, op := range s.membatch.nodes {
428+
if op.isDelete() {
429+
// node deletion is only supported in path mode.
430+
if op.owner == (common.Hash{}) {
431+
rawdb.DeleteAccountTrieNode(dbw, op.path)
432+
} else {
433+
rawdb.DeleteStorageTrieNode(dbw, op.owner, op.path)
434+
}
435+
deletionGauge.Inc(1)
396436
} else {
397-
storage += 1
437+
if op.owner == (common.Hash{}) {
438+
account += 1
439+
} else {
440+
storage += 1
441+
}
442+
rawdb.WriteTrieNode(dbw, op.owner, op.path, op.hash, op.blob, s.scheme)
398443
}
399-
rawdb.WriteTrieNode(dbw, owner, inner, s.membatch.hashes[path], value, s.scheme)
400-
hash := s.membatch.hashes[path]
444+
hash := op.hash
401445
if s.bloom != nil {
402446
s.bloom.Add(hash[:])
403447
}
404448
}
405449
accountNodeSyncedGauge.Inc(int64(account))
406450
storageNodeSyncedGauge.Inc(int64(storage))
407451

408-
// Flush the pending node deletes into the database batch.
409-
// Please note that each written and deleted node has a
410-
// unique path, ensuring no duplication occurs.
411-
for path := range s.membatch.deletes {
412-
owner, inner := ResolvePath([]byte(path))
413-
rawdb.DeleteTrieNode(dbw, owner, inner, common.Hash{} /* unused */, s.scheme)
414-
}
415452
// Flush the pending code writes into database batch.
416453
for hash, value := range s.membatch.codes {
417454
rawdb.WriteCode(dbw, hash, value)
@@ -421,7 +458,7 @@ func (s *Sync) Commit(dbw ethdb.Batch) error {
421458
}
422459
codeSyncedGauge.Inc(int64(len(s.membatch.codes)))
423460

424-
s.membatch = newSyncMemBatch() // reset the batch
461+
s.membatch = newSyncMemBatch(s.scheme) // reset the batch
425462
return nil
426463
}
427464

@@ -489,12 +526,15 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
489526
// child as invalid. This is essential in the case of path mode
490527
// scheme; otherwise, state healing might overwrite existing child
491528
// nodes silently while leaving a dangling parent node within the
492-
// range of this internal path on disk. This would break the
493-
// guarantee for state healing.
529+
// range of this internal path on disk and the persistent state
530+
// ends up with a very weird situation that nodes on the same path
531+
// are not inconsistent while they all present in disk. This property
532+
// would break the guarantee for state healing.
494533
//
495534
// While it's possible for this shortNode to overwrite a previously
496535
// existing full node, the other branches of the fullNode can be
497-
// retained as they remain untouched and complete.
536+
// retained as they are not accessible with the new shortNode, and
537+
// also the whole sub-trie is still untouched and complete.
498538
//
499539
// This step is only necessary for path mode, as there is no deletion
500540
// in hash mode at all.
@@ -511,8 +551,7 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
511551
exists = rawdb.ExistsStorageTrieNode(s.database, owner, append(inner, key[:i]...))
512552
}
513553
if exists {
514-
req.deletes = append(req.deletes, key[:i])
515-
deletionGauge.Inc(1)
554+
s.membatch.delNode(owner, append(inner, key[:i]...))
516555
log.Debug("Detected dangling node", "owner", owner, "path", append(inner, key[:i]...))
517556
}
518557
}
@@ -532,6 +571,7 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
532571
}
533572
// Iterate over the children, and request all unknown ones
534573
requests := make([]*nodeRequest, 0, len(children))
574+
var batchMu sync.Mutex
535575
for _, child := range children {
536576
// Notify any external watcher of a new key/value node
537577
if req.callback != nil {
@@ -548,29 +588,33 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
548588
}
549589
}
550590
}
551-
// If the child references another node, resolve or schedule
591+
// If the child references another node, resolve or schedule.
592+
// We check all children concurrently.
552593
if node, ok := (child.node).(hashNode); ok {
553-
// Try to resolve the node from the local database
554-
if s.membatch.hasNode(child.path) {
555-
continue
556-
}
557-
chash := common.BytesToHash(node)
594+
path := child.path
595+
hash := common.BytesToHash(node)
558596
if s.bloom == nil || s.bloom.Contains(node) {
559597
// Bloom filter says this might be a duplicate, double check.
560598
// If database says yes, then at least the trie node is present
561599
// and we hold the assumption that it's NOT legacy contract code.
562-
owner, inner := ResolvePath(child.path)
563-
if rawdb.HasTrieNode(s.database, owner, inner, chash, s.scheme) {
600+
owner, inner := ResolvePath(path)
601+
exist, inconsistent := s.hasNode(owner, inner, hash)
602+
if exist {
564603
continue
604+
} else if inconsistent {
605+
// There is a pre-existing node with the wrong hash in DB, remove it.
606+
batchMu.Lock()
607+
s.membatch.delNode(owner, inner)
608+
batchMu.Unlock()
565609
}
566610

567611
// False positive, bump fault meter
568612
bloomFaultMeter.Mark(1)
569613
}
570614
// Locally unknown node, schedule for retrieval
571615
requests = append(requests, &nodeRequest{
572-
path: child.path,
573-
hash: chash,
616+
path: path,
617+
hash: hash,
574618
parent: req,
575619
callback: req.callback,
576620
})
@@ -584,14 +628,10 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
584628
// committed themselves.
585629
func (s *Sync) commitNodeRequest(req *nodeRequest) error {
586630
// Write the node content to the membatch
587-
s.membatch.nodes[string(req.path)] = req.data
588-
s.membatch.hashes[string(req.path)] = req.hash
631+
owner, path := ResolvePath(req.path)
632+
s.membatch.addNode(owner, path, req.data, req.hash)
589633

590-
// Delete the internal nodes which are marked as invalid
591-
for _, segment := range req.deletes {
592-
path := append(req.path, segment...)
593-
s.membatch.deletes[string(path)] = struct{}{}
594-
}
634+
// Removed the completed node request
595635
delete(s.nodeReqs, string(req.path))
596636
s.fetches[len(req.path)]--
597637

@@ -612,7 +652,9 @@ func (s *Sync) commitNodeRequest(req *nodeRequest) error {
612652
// committed themselves.
613653
func (s *Sync) commitCodeRequest(req *codeRequest) error {
614654
// Write the node content to the membatch
615-
s.membatch.codes[req.hash] = req.data
655+
s.membatch.addCode(req.hash, req.data)
656+
657+
// Removed the completed code request
616658
delete(s.codeReqs, req.hash)
617659
s.fetches[len(req.path)]--
618660

@@ -628,6 +670,28 @@ func (s *Sync) commitCodeRequest(req *codeRequest) error {
628670
return nil
629671
}
630672

673+
// hasNode reports whether the specified trie node is present in the database.
674+
// 'exists' is true when the node exists in the database and matches the given root
675+
// hash. The 'inconsistent' return value is true when the node exists but does not
676+
// match the expected hash.
677+
func (s *Sync) hasNode(owner common.Hash, path []byte, hash common.Hash) (exists bool, inconsistent bool) {
678+
// If node is running with hash scheme, check the presence with node hash.
679+
if s.scheme == rawdb.HashScheme {
680+
return rawdb.HasLegacyTrieNode(s.database, hash), false
681+
}
682+
// If node is running with path scheme, check the presence with node path.
683+
var blob []byte
684+
var dbHash common.Hash
685+
if owner == (common.Hash{}) {
686+
blob, dbHash = rawdb.ReadAccountTrieNode(s.database, path)
687+
} else {
688+
blob, dbHash = rawdb.ReadStorageTrieNode(s.database, owner, path)
689+
}
690+
exists = hash == dbHash
691+
inconsistent = !exists && len(blob) != 0
692+
return exists, inconsistent
693+
}
694+
631695
// ResolvePath resolves the provided composite node path by separating the
632696
// path in account trie if it's existent.
633697
func ResolvePath(path []byte) (common.Hash, []byte) {

0 commit comments

Comments
 (0)