Skip to content

Commit 4773dcb

Browse files
authored
trie: remove internal nodes between shortNode and child in path mode (#28163)
* trie: remove internal nodes between shortNode and child in path mode * trie: address comments * core/rawdb, trie: address comments * core/rawdb: delete unused func * trie: change comments * trie: add missing tests * trie: fix lint
1 parent 545f4c5 commit 4773dcb

File tree

3 files changed

+238
-40
lines changed

3 files changed

+238
-40
lines changed

core/rawdb/accessors_trie.go

+20
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ func HasAccountTrieNode(db ethdb.KeyValueReader, path []byte, hash common.Hash)
8989
return h.hash(data) == hash
9090
}
9191

92+
// ExistsAccountTrieNode checks the presence of the account trie node with the
93+
// specified node path, regardless of the node hash.
94+
func ExistsAccountTrieNode(db ethdb.KeyValueReader, path []byte) bool {
95+
has, err := db.Has(accountTrieNodeKey(path))
96+
if err != nil {
97+
return false
98+
}
99+
return has
100+
}
101+
92102
// WriteAccountTrieNode writes the provided account trie node into database.
93103
func WriteAccountTrieNode(db ethdb.KeyValueWriter, path []byte, node []byte) {
94104
if err := db.Put(accountTrieNodeKey(path), node); err != nil {
@@ -127,6 +137,16 @@ func HasStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path [
127137
return h.hash(data) == hash
128138
}
129139

140+
// ExistsStorageTrieNode checks the presence of the storage trie node with the
141+
// specified account hash and node path, regardless of the node hash.
142+
func ExistsStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) bool {
143+
has, err := db.Has(storageTrieNodeKey(accountHash, path))
144+
if err != nil {
145+
return false
146+
}
147+
return has
148+
}
149+
130150
// WriteStorageTrieNode writes the provided storage trie node into database.
131151
func WriteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte, node []byte) {
132152
if err := db.Put(storageTrieNodeKey(accountHash, path), node); err != nil {

trie/sync.go

+79-16
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/ethereum/go-ethereum/core/types"
2828
"github.com/ethereum/go-ethereum/ethdb"
2929
"github.com/ethereum/go-ethereum/log"
30+
"github.com/ethereum/go-ethereum/metrics"
3031
)
3132

3233
// ErrNotRequested is returned by the trie sync when it's requested to process a
@@ -42,6 +43,16 @@ var ErrAlreadyProcessed = errors.New("already processed")
4243
// memory if the node was configured with a significant number of peers.
4344
const maxFetchesPerDepth = 16384
4445

46+
var (
47+
// deletionGauge is the metric to track how many trie node deletions
48+
// are performed in total during the sync process.
49+
deletionGauge = metrics.NewRegisteredGauge("trie/sync/delete", nil)
50+
51+
// lookupGauge is the metric to track how many trie node lookups are
52+
// performed to determine if node needs to be deleted.
53+
lookupGauge = metrics.NewRegisteredGauge("trie/sync/lookup", nil)
54+
)
55+
4556
// SyncPath is a path tuple identifying a particular trie node either in a single
4657
// trie (account) or a layered trie (account -> storage).
4758
//
@@ -93,9 +104,10 @@ type LeafCallback func(keys [][]byte, path []byte, leaf []byte, parent common.Ha
93104

94105
// nodeRequest represents a scheduled or already in-flight trie node retrieval request.
95106
type nodeRequest struct {
96-
hash common.Hash // Hash of the trie node to retrieve
97-
path []byte // Merkle path leading to this node for prioritization
98-
data []byte // Data content of the node, cached until all subtrees complete
107+
hash common.Hash // Hash of the trie node to retrieve
108+
path []byte // Merkle path leading to this node for prioritization
109+
data []byte // Data content of the node, cached until all subtrees complete
110+
deletes [][]byte // List of internal path segments for trie nodes to delete
99111

100112
parent *nodeRequest // Parent state node referencing this entry
101113
deps int // Number of dependencies before allowed to commit this node
@@ -125,18 +137,20 @@ type CodeSyncResult struct {
125137
// syncMemBatch is an in-memory buffer of successfully downloaded but not yet
126138
// persisted data items.
127139
type syncMemBatch struct {
128-
nodes map[string][]byte // In-memory membatch of recently completed nodes
129-
hashes map[string]common.Hash // Hashes of recently completed nodes
130-
codes map[common.Hash][]byte // In-memory membatch of recently completed codes
131-
size uint64 // Estimated batch-size of in-memory data.
140+
nodes map[string][]byte // In-memory membatch of recently completed nodes
141+
hashes map[string]common.Hash // Hashes of recently completed nodes
142+
deletes map[string]struct{} // List of paths for trie node to delete
143+
codes map[common.Hash][]byte // In-memory membatch of recently completed codes
144+
size uint64 // Estimated batch-size of in-memory data.
132145
}
133146

134147
// newSyncMemBatch allocates a new memory-buffer for not-yet persisted trie nodes.
135148
func newSyncMemBatch() *syncMemBatch {
136149
return &syncMemBatch{
137-
nodes: make(map[string][]byte),
138-
hashes: make(map[string]common.Hash),
139-
codes: make(map[common.Hash][]byte),
150+
nodes: make(map[string][]byte),
151+
hashes: make(map[string]common.Hash),
152+
deletes: make(map[string]struct{}),
153+
codes: make(map[common.Hash][]byte),
140154
}
141155
}
142156

@@ -347,16 +361,23 @@ func (s *Sync) ProcessNode(result NodeSyncResult) error {
347361
// Commit flushes the data stored in the internal membatch out to persistent
348362
// storage, returning any occurred error.
349363
func (s *Sync) Commit(dbw ethdb.Batch) error {
350-
// Dump the membatch into a database dbw
364+
// Flush the pending node writes into database batch.
351365
for path, value := range s.membatch.nodes {
352366
owner, inner := ResolvePath([]byte(path))
353367
rawdb.WriteTrieNode(dbw, owner, inner, s.membatch.hashes[path], value, s.scheme)
354368
}
369+
// Flush the pending node deletes into the database batch.
370+
// Please note that each written and deleted node has a
371+
// unique path, ensuring no duplication occurs.
372+
for path := range s.membatch.deletes {
373+
owner, inner := ResolvePath([]byte(path))
374+
rawdb.DeleteTrieNode(dbw, owner, inner, common.Hash{} /* unused */, s.scheme)
375+
}
376+
// Flush the pending code writes into database batch.
355377
for hash, value := range s.membatch.codes {
356378
rawdb.WriteCode(dbw, hash, value)
357379
}
358-
// Drop the membatch data and return
359-
s.membatch = newSyncMemBatch()
380+
s.membatch = newSyncMemBatch() // reset the batch
360381
return nil
361382
}
362383

@@ -425,6 +446,39 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
425446
node: node.Val,
426447
path: append(append([]byte(nil), req.path...), key...),
427448
}}
449+
// Mark all internal nodes between shortNode and its **in disk**
450+
// child as invalid. This is essential in the case of path mode
451+
// scheme; otherwise, state healing might overwrite existing child
452+
// nodes silently while leaving a dangling parent node within the
453+
// range of this internal path on disk. This would break the
454+
// guarantee for state healing.
455+
//
456+
// While it's possible for this shortNode to overwrite a previously
457+
// existing full node, the other branches of the fullNode can be
458+
// retained as they remain untouched and complete.
459+
//
460+
// This step is only necessary for path mode, as there is no deletion
461+
// in hash mode at all.
462+
if _, ok := node.Val.(hashNode); ok && s.scheme == rawdb.PathScheme {
463+
owner, inner := ResolvePath(req.path)
464+
for i := 1; i < len(key); i++ {
465+
// While checking for a non-existent item in Pebble can be less efficient
466+
// without a bloom filter, the relatively low frequency of lookups makes
467+
// the performance impact negligible.
468+
var exists bool
469+
if owner == (common.Hash{}) {
470+
exists = rawdb.ExistsAccountTrieNode(s.database, append(inner, key[:i]...))
471+
} else {
472+
exists = rawdb.ExistsStorageTrieNode(s.database, owner, append(inner, key[:i]...))
473+
}
474+
if exists {
475+
req.deletes = append(req.deletes, key[:i])
476+
deletionGauge.Inc(1)
477+
log.Debug("Detected dangling node", "owner", owner, "path", append(inner, key[:i]...))
478+
}
479+
}
480+
lookupGauge.Inc(int64(len(key) - 1))
481+
}
428482
case *fullNode:
429483
for i := 0; i < 17; i++ {
430484
if node.Children[i] != nil {
@@ -509,10 +563,19 @@ func (s *Sync) commitNodeRequest(req *nodeRequest) error {
509563
// Write the node content to the membatch
510564
s.membatch.nodes[string(req.path)] = req.data
511565
s.membatch.hashes[string(req.path)] = req.hash
566+
512567
// The size tracking refers to the db-batch, not the in-memory data.
513-
// Therefore, we ignore the req.path, and account only for the hash+data
514-
// which eventually is written to db.
515-
s.membatch.size += common.HashLength + uint64(len(req.data))
568+
if s.scheme == rawdb.PathScheme {
569+
s.membatch.size += uint64(len(req.path) + len(req.data))
570+
} else {
571+
s.membatch.size += common.HashLength + uint64(len(req.data))
572+
}
573+
// Delete the internal nodes which are marked as invalid
574+
for _, segment := range req.deletes {
575+
path := append(req.path, segment...)
576+
s.membatch.deletes[string(path)] = struct{}{}
577+
s.membatch.size += uint64(len(path))
578+
}
516579
delete(s.nodeReqs, string(req.path))
517580
s.fetches[len(req.path)]--
518581

0 commit comments

Comments
 (0)