From 350a82c582e11800dfa28a0bbc3c4d4b44d9fe36 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 20 Aug 2024 15:21:20 +0800 Subject: [PATCH 1/7] triedb/pathdb: introduce lookup structure to optimize state access --- triedb/pathdb/database.go | 4 +- triedb/pathdb/difflayer.go | 4 +- triedb/pathdb/disklayer.go | 9 - triedb/pathdb/layertree.go | 177 +++++-- triedb/pathdb/layertree_test.go | 885 ++++++++++++++++++++++++++++++++ triedb/pathdb/lookup.go | 281 ++++++++++ triedb/pathdb/metrics.go | 3 + triedb/pathdb/reader.go | 46 +- 8 files changed, 1358 insertions(+), 51 deletions(-) create mode 100644 triedb/pathdb/layertree_test.go create mode 100644 triedb/pathdb/lookup.go diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index c20646031596..3174a7c964b3 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -487,7 +487,7 @@ func (db *Database) Enable(root common.Hash) error { // Re-construct a new disk layer backed by persistent state // and schedule the state snapshot generation if it's permitted. - db.tree.reset(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild)) + db.tree.init(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild)) log.Info("Rebuilt trie database", "root", root) return nil } @@ -529,7 +529,7 @@ func (db *Database) Recover(root common.Hash) error { // reset layer with newly created disk layer. It must be // done after each revert operation, otherwise the new // disk layer won't be accessible from outside. - db.tree.reset(dl) + db.tree.init(dl) } rawdb.DeleteTrieJournal(db.diskdb) diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go index 2e1d83c74a94..ac05b4a0fbeb 100644 --- a/triedb/pathdb/difflayer.go +++ b/triedb/pathdb/difflayer.go @@ -156,7 +156,7 @@ func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes *no } // persist flushes the diff layer and all its parent layers to disk layer. -func (dl *diffLayer) persist(force bool) (layer, error) { +func (dl *diffLayer) persist(force bool) (*diskLayer, error) { if parent, ok := dl.parentLayer().(*diffLayer); ok { // Hold the lock to prevent any read operation until the new // parent is linked correctly. @@ -183,7 +183,7 @@ func (dl *diffLayer) size() uint64 { // diffToDisk merges a bottom-most diff into the persistent disk layer underneath // it. The method will panic if called onto a non-bottom-most diff layer. -func diffToDisk(layer *diffLayer, force bool) (layer, error) { +func diffToDisk(layer *diffLayer, force bool) (*diskLayer, error) { disk, ok := layer.parentLayer().(*diskLayer) if !ok { panic(fmt.Sprintf("unknown layer type: %T", layer.parentLayer())) diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index bee4cd1e8a4f..1c9efb024bd6 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -94,15 +94,6 @@ func (dl *diskLayer) setGenerator(generator *generator) { dl.generator = generator } -// isStale return whether this layer has become stale (was flattened across) or if -// it's still live. -func (dl *diskLayer) isStale() bool { - dl.lock.RLock() - defer dl.lock.RUnlock() - - return dl.stale -} - // markStale sets the stale flag as true. func (dl *diskLayer) markStale() { dl.lock.Lock() diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index 85a5e470e7f9..ef0cd65d6f2b 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -31,29 +31,41 @@ import ( // thread-safe to use. However, callers need to ensure the thread-safety // of the referenced layer by themselves. type layerTree struct { - lock sync.RWMutex - layers map[common.Hash]layer + base *diskLayer + layers map[common.Hash]layer + descendants map[common.Hash]map[common.Hash]struct{} + lookup *lookup + lock sync.RWMutex } // newLayerTree constructs the layerTree with the given head layer. func newLayerTree(head layer) *layerTree { tree := new(layerTree) - tree.reset(head) + tree.init(head) return tree } -// reset initializes the layerTree by the given head layer. -// All the ancestors will be iterated out and linked in the tree. -func (tree *layerTree) reset(head layer) { +// init initializes the layerTree by the given head layer. +func (tree *layerTree) init(head layer) { tree.lock.Lock() defer tree.lock.Unlock() - var layers = make(map[common.Hash]layer) - for head != nil { - layers[head.rootHash()] = head - head = head.parentLayer() + current := head + tree.layers = make(map[common.Hash]layer) + tree.descendants = make(map[common.Hash]map[common.Hash]struct{}) + + for { + tree.layers[current.rootHash()] = current + tree.fillAncestors(current) + + parent := current.parentLayer() + if parent == nil { + break + } + current = parent } - tree.layers = layers + tree.base = current.(*diskLayer) // panic if it's not a disk layer + tree.lookup = newLookup(head, tree.isDescendant) } // get retrieves a layer belonging to the given state root. @@ -64,6 +76,43 @@ func (tree *layerTree) get(root common.Hash) layer { return tree.layers[root] } +// isDescendant returns whether the specified layer with given root is a +// descendant of a specific ancestor. +// +// This function assumes the read lock has been held. +func (tree *layerTree) isDescendant(root common.Hash, ancestor common.Hash) bool { + subset := tree.descendants[ancestor] + if subset == nil { + return false + } + _, ok := subset[root] + return ok +} + +// fillAncestors identifies the ancestors of the given layer and populates the +// descendants set. The ancestors include the diff layers below the supplied +// layer and also the disk layer. +// +// This function assumes the write lock has been held. +func (tree *layerTree) fillAncestors(layer layer) { + hash := layer.rootHash() + for { + parent := layer.parentLayer() + if parent == nil { + break + } + layer = parent + + phash := parent.rootHash() + subset := tree.descendants[phash] + if subset == nil { + subset = make(map[common.Hash]struct{}) + tree.descendants[phash] = subset + } + subset[hash] = struct{}{} + } +} + // forEach iterates the stored layers inside and applies the // given callback on them. func (tree *layerTree) forEach(onLayer func(layer)) { @@ -101,8 +150,11 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6 l := parent.update(root, parent.stateID()+1, block, newNodeSet(nodes.Flatten()), states) tree.lock.Lock() + defer tree.lock.Unlock() + tree.layers[l.rootHash()] = l - tree.lock.Unlock() + tree.fillAncestors(l) + tree.lookup.addLayer(l) return nil } @@ -127,8 +179,14 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { if err != nil { return err } - // Replace the entire layer tree with the flat base - tree.layers = map[common.Hash]layer{base.rootHash(): base} + tree.base = base + + // Reset the layer tree with the single new disk layer + tree.layers = map[common.Hash]layer{ + base.rootHash(): base, + } + tree.descendants = make(map[common.Hash]map[common.Hash]struct{}) + tree.lookup = newLookup(base, tree.isDescendant) return nil } // Dive until we run out of layers or reach the persistent database @@ -143,6 +201,11 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { } // We're out of layers, flatten anything below, stopping if it's the disk or if // the memory limit is not yet exceeded. + var ( + err error + replaced layer + newBase *diskLayer + ) switch parent := diff.parentLayer().(type) { case *diskLayer: return nil @@ -152,14 +215,33 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { // parent is linked correctly. diff.lock.Lock() - base, err := parent.persist(false) + // Hold the reference of the original layer being replaced + replaced = parent + + // Replace the original parent layer with new disk layer. The procedure + // can be illustrated as below: + // + // Before change: + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C2'->C3'->C4' + // + // After change: + // Chain: + // (a) C3->C4 (HEAD) + // (b) C1->C2 + // ->C2'->C3'->C4' + // The original C3 is replaced by the new base (with root C3) + // Dangling layers in (b) will be removed later + newBase, err = parent.persist(false) if err != nil { diff.lock.Unlock() return err } - tree.layers[base.rootHash()] = base - diff.parent = base + tree.layers[newBase.rootHash()] = newBase + // Link the new parent and release the lock + diff.parent = newBase diff.lock.Unlock() default: @@ -173,19 +255,28 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { children[parent] = append(children[parent], root) } } + clearDiff := func(layer layer) { + diff, ok := layer.(*diffLayer) + if !ok { + return + } + tree.lookup.removeLayer(diff) + } var remove func(root common.Hash) remove = func(root common.Hash) { + clearDiff(tree.layers[root]) + + // Unlink the layer from the layer tree and cascade to its children + delete(tree.descendants, root) delete(tree.layers, root) for _, child := range children[root] { remove(child) } delete(children, root) } - for root, layer := range tree.layers { - if dl, ok := layer.(*diskLayer); ok && dl.isStale() { - remove(root) - } - } + remove(tree.base.rootHash()) // remove the old/stale disk layer + clearDiff(replaced) // remove the lookup data of the stale parent being replaced + tree.base = newBase // update the base layer with newly constructed one return nil } @@ -194,17 +285,39 @@ func (tree *layerTree) bottom() *diskLayer { tree.lock.RLock() defer tree.lock.RUnlock() - if len(tree.layers) == 0 { - return nil // Shouldn't happen, empty tree + return tree.base +} + +// lookupAccount returns the layer that is confirmed to contain the account data +// being searched for. +func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) (layer, error) { + tree.lock.RLock() + defer tree.lock.RUnlock() + + tip := tree.lookup.accountTip(accountHash, state, tree.base.root) + if tip == (common.Hash{}) { + return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale) } - // pick a random one as the entry point - var current layer - for _, layer := range tree.layers { - current = layer - break + l := tree.layers[tip] + if l == nil { + return nil, fmt.Errorf("triedb layer [%#x] missing", tip) } - for current.parentLayer() != nil { - current = current.parentLayer() + return l, nil +} + +// lookupStorage returns the layer that is confirmed to contain the storage slot +// data being searched for. +func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Hash, state common.Hash) (layer, error) { + tree.lock.RLock() + defer tree.lock.RUnlock() + + tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root) + if tip == (common.Hash{}) { + return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale) + } + l := tree.layers[tip] + if l == nil { + return nil, fmt.Errorf("triedb layer [%#x] missing", tip) } - return current.(*diskLayer) + return l, nil } diff --git a/triedb/pathdb/layertree_test.go b/triedb/pathdb/layertree_test.go new file mode 100644 index 000000000000..a72ff5899eed --- /dev/null +++ b/triedb/pathdb/layertree_test.go @@ -0,0 +1,885 @@ +// Copyright 2024 The 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 + +package pathdb + +import ( + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/trie/trienode" +) + +func newTestLayerTree() *layerTree { + db := New(rawdb.NewMemoryDatabase(), nil, false) + l := newDiskLayer(common.Hash{0x1}, 0, db, nil, nil, newBuffer(0, nil, nil, 0)) + t := newLayerTree(l) + return t +} + +func TestLayerCap(t *testing.T) { + var cases = []struct { + init func() *layerTree + head common.Hash + layers int + base common.Hash + snapshot map[common.Hash]struct{} + }{ + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C2->C3->C4 (HEAD) + head: common.Hash{0x4}, + layers: 2, + base: common.Hash{0x2}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x2}: {}, + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C3->C4 (HEAD) + head: common.Hash{0x4}, + layers: 1, + base: common.Hash{0x3}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C4 (HEAD) + head: common.Hash{0x4}, + layers: 0, + base: common.Hash{0x4}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x4}: {}, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C2'->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C2->C3->C4 (HEAD) + head: common.Hash{0x4a}, + layers: 2, + base: common.Hash{0x2a}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x4a}: {}, + common.Hash{0x3a}: {}, + common.Hash{0x2a}: {}, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C2'->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C3->C4 (HEAD) + head: common.Hash{0x4a}, + layers: 1, + base: common.Hash{0x3a}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x4a}: {}, + common.Hash{0x3a}: {}, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + // Chain: + // C2->C3->C4 (HEAD) + // ->C3'->C4' + head: common.Hash{0x4a}, + layers: 2, + base: common.Hash{0x2}, + snapshot: map[common.Hash]struct{}{ + common.Hash{0x4a}: {}, + common.Hash{0x3a}: {}, + common.Hash{0x4b}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x2}: {}, + }, + }, + } + for _, c := range cases { + tr := c.init() + if err := tr.cap(c.head, c.layers); err != nil { + t.Fatalf("Failed to cap the layer tree %v", err) + } + if tr.bottom().root != c.base { + t.Fatalf("Unexpected bottom layer tree root, want %v, got %v", c.base, tr.bottom().root) + } + if len(c.snapshot) != len(tr.layers) { + t.Fatalf("Unexpected layer tree size, want %v, got %v", len(c.snapshot), len(tr.layers)) + } + for h := range tr.layers { + if _, ok := c.snapshot[h]; !ok { + t.Fatalf("Unexpected layer %v", h) + } + } + } +} + +func TestBaseLayer(t *testing.T) { + tr := newTestLayerTree() + + var cases = []struct { + op func() + base common.Hash + }{ + // Chain: + // C1 (HEAD) + { + func() {}, + common.Hash{0x1}, + }, + // Chain: + // C1->C2->C3 (HEAD) + { + func() { + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + }, + common.Hash{0x1}, + }, + // Chain: + // C3 (HEAD) + { + func() { + tr.cap(common.Hash{0x3}, 0) + }, + common.Hash{0x3}, + }, + // Chain: + // C4->C5->C6 (HEAD) + { + func() { + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x5}, common.Hash{0x4}, 4, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x6}, common.Hash{0x5}, 5, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.cap(common.Hash{0x6}, 2) + }, + common.Hash{0x4}, + }, + } + for _, c := range cases { + c.op() + if tr.base.rootHash() != c.base { + t.Fatalf("Unexpected base root, want %v, got: %v", c.base, tr.base.rootHash()) + } + } +} + +func TestDescendant(t *testing.T) { + var cases = []struct { + init func() *layerTree + snapshotA map[common.Hash]map[common.Hash]struct{} + op func(tr *layerTree) + snapshotB map[common.Hash]map[common.Hash]struct{} + }{ + { + // Chain: + // C1->C2 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + }, + }, + // Chain: + // C1->C2->C3 (HEAD) + op: func(tr *layerTree) { + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + common.Hash{0x3}: {}, + }, + common.Hash{0x2}: { + common.Hash{0x3}: {}, + }, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x2}: { + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x3}: { + common.Hash{0x4}: {}, + }, + }, + // Chain: + // C2->C3->C4 (HEAD) + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4}, 2) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x2}: { + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x3}: { + common.Hash{0x4}: {}, + }, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x2}: { + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x3}: { + common.Hash{0x4}: {}, + }, + }, + // Chain: + // C3->C4 (HEAD) + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4}, 1) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x3}: { + common.Hash{0x4}: {}, + }, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x2}: { + common.Hash{0x3}: {}, + common.Hash{0x4}: {}, + }, + common.Hash{0x3}: { + common.Hash{0x4}: {}, + }, + }, + // Chain: + // C4 (HEAD) + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4}, 0) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{}, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C2'->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2a}: {}, + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + common.Hash{0x2b}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x2a}: { + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + }, + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + common.Hash{0x2b}: { + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x3b}: { + common.Hash{0x4b}: {}, + }, + }, + // Chain: + // C2->C3->C4 (HEAD) + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4a}, 2) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x2a}: { + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + }, + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C2'->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2a}: {}, + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + common.Hash{0x2b}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x2a}: { + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + }, + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + common.Hash{0x2b}: { + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x3b}: { + common.Hash{0x4b}: {}, + }, + }, + // Chain: + // C3->C4 (HEAD) + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4a}, 1) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + }, + }, + { + // Chain: + // C1->C2->C3->C4 (HEAD) + // ->C3'->C4' + init: func() *layerTree { + tr := newTestLayerTree() + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false)) + return tr + }, + snapshotA: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x1}: { + common.Hash{0x2}: {}, + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x2}: { + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + common.Hash{0x3b}: { + common.Hash{0x4b}: {}, + }, + }, + // Chain: + // C2->C3->C4 (HEAD) + // ->C3'->C4' + op: func(tr *layerTree) { + tr.cap(common.Hash{0x4a}, 2) + }, + snapshotB: map[common.Hash]map[common.Hash]struct{}{ + common.Hash{0x2}: { + common.Hash{0x3a}: {}, + common.Hash{0x4a}: {}, + common.Hash{0x3b}: {}, + common.Hash{0x4b}: {}, + }, + common.Hash{0x3a}: { + common.Hash{0x4a}: {}, + }, + common.Hash{0x3b}: { + common.Hash{0x4b}: {}, + }, + }, + }, + } + check := func(setA, setB map[common.Hash]map[common.Hash]struct{}) bool { + if len(setA) != len(setB) { + return false + } + for h, subA := range setA { + subB, ok := setB[h] + if !ok { + return false + } + if len(subA) != len(subB) { + return false + } + for hh := range subA { + if _, ok := subB[hh]; !ok { + return false + } + } + } + return true + } + for _, c := range cases { + tr := c.init() + if !check(c.snapshotA, tr.descendants) { + t.Fatalf("Unexpected descendants") + } + c.op(tr) + if !check(c.snapshotB, tr.descendants) { + t.Fatalf("Unexpected descendants") + } + } +} + +func TestAccountLookup(t *testing.T) { + // Chain: + // C1->C2->C3->C4 (HEAD) + tr := newTestLayerTree() // base = 0x1 + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xa"), nil, nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xb"), nil, nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xa", "0xc"), nil, nil, nil, false)) + + var cases = []struct { + account common.Hash + state common.Hash + expect common.Hash + }{ + { + // unknown account + common.HexToHash("0xd"), common.Hash{0x4}, common.Hash{0x1}, + }, + /* + lookup account from the top + */ + { + common.HexToHash("0xa"), common.Hash{0x4}, common.Hash{0x4}, + }, + { + common.HexToHash("0xb"), common.Hash{0x4}, common.Hash{0x3}, + }, + { + common.HexToHash("0xc"), common.Hash{0x4}, common.Hash{0x4}, + }, + /* + lookup account from the middle + */ + { + common.HexToHash("0xa"), common.Hash{0x3}, common.Hash{0x2}, + }, + { + common.HexToHash("0xb"), common.Hash{0x3}, common.Hash{0x3}, + }, + { + common.HexToHash("0xc"), common.Hash{0x3}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0xa"), common.Hash{0x2}, common.Hash{0x2}, + }, + { + common.HexToHash("0xb"), common.Hash{0x2}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0xc"), common.Hash{0x2}, common.Hash{0x1}, // not found + }, + /* + lookup account from the bottom + */ + { + common.HexToHash("0xa"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0xb"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0xc"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + } + for i, c := range cases { + l, err := tr.lookupAccount(c.account, c.state) + if err != nil { + t.Fatalf("%d: %v", i, err) + } + if l.rootHash() != c.expect { + t.Errorf("Unexpected tiphash, %d, want: %x, got: %x", i, c.expect, l.rootHash()) + } + } + + // Chain: + // C3->C4 (HEAD) + tr.cap(common.Hash{0x4}, 1) + + cases2 := []struct { + account common.Hash + state common.Hash + expect common.Hash + expectErr error + }{ + { + // unknown account + common.HexToHash("0xd"), common.Hash{0x4}, common.Hash{0x3}, nil, + }, + /* + lookup account from the top + */ + { + common.HexToHash("0xa"), common.Hash{0x4}, common.Hash{0x4}, nil, + }, + { + common.HexToHash("0xb"), common.Hash{0x4}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0xc"), common.Hash{0x4}, common.Hash{0x4}, nil, + }, + /* + lookup account from the bottom + */ + { + common.HexToHash("0xa"), common.Hash{0x3}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0xb"), common.Hash{0x3}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0xc"), common.Hash{0x3}, common.Hash{0x3}, nil, // not found + }, + /* + stale states + */ + { + common.HexToHash("0xa"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0xb"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0xc"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0xa"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0xb"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0xc"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + } + for i, c := range cases2 { + l, err := tr.lookupAccount(c.account, c.state) + if c.expectErr != nil { + if !errors.Is(err, c.expectErr) { + t.Fatalf("%d: unexpected error, want %v, got %v", i, c.expectErr, err) + } + } + if c.expectErr == nil { + if err != nil { + t.Fatalf("%d: %v", i, err) + } + if l.rootHash() != c.expect { + t.Errorf("Unexpected tiphash, %d, want: %x, got: %x", i, c.expect, l.rootHash()) + } + } + } +} + +func TestStorageLookup(t *testing.T) { + // Chain: + // C1->C2->C3->C4 (HEAD) + tr := newTestLayerTree() // base = 0x1 + tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false)) + tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x2"}}, nil), nil, nil, false)) + tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), + NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1", "0x3"}}, nil), nil, nil, false)) + + var cases = []struct { + storage common.Hash + state common.Hash + expect common.Hash + }{ + { + // unknown storage slot + common.HexToHash("0x4"), common.Hash{0x4}, common.Hash{0x1}, + }, + /* + lookup storage slot from the top + */ + { + common.HexToHash("0x1"), common.Hash{0x4}, common.Hash{0x4}, + }, + { + common.HexToHash("0x2"), common.Hash{0x4}, common.Hash{0x3}, + }, + { + common.HexToHash("0x3"), common.Hash{0x4}, common.Hash{0x4}, + }, + /* + lookup storage slot from the middle + */ + { + common.HexToHash("0x1"), common.Hash{0x3}, common.Hash{0x2}, + }, + { + common.HexToHash("0x2"), common.Hash{0x3}, common.Hash{0x3}, + }, + { + common.HexToHash("0x3"), common.Hash{0x3}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0x1"), common.Hash{0x2}, common.Hash{0x2}, + }, + { + common.HexToHash("0x2"), common.Hash{0x2}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0x3"), common.Hash{0x2}, common.Hash{0x1}, // not found + }, + /* + lookup storage slot from the bottom + */ + { + common.HexToHash("0x1"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0x2"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + { + common.HexToHash("0x3"), common.Hash{0x1}, common.Hash{0x1}, // not found + }, + } + for i, c := range cases { + l, err := tr.lookupStorage(common.HexToHash("0xa"), c.storage, c.state) + if err != nil { + t.Fatalf("%d: %v", i, err) + } + if l.rootHash() != c.expect { + t.Errorf("Unexpected tiphash, %d, want: %x, got: %x", i, c.expect, l.rootHash()) + } + } + + // Chain: + // C3->C4 (HEAD) + tr.cap(common.Hash{0x4}, 1) + + cases2 := []struct { + storage common.Hash + state common.Hash + expect common.Hash + expectErr error + }{ + { + // unknown storage slot + common.HexToHash("0x4"), common.Hash{0x4}, common.Hash{0x3}, nil, + }, + /* + lookup account from the top + */ + { + common.HexToHash("0x1"), common.Hash{0x4}, common.Hash{0x4}, nil, + }, + { + common.HexToHash("0x2"), common.Hash{0x4}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0x3"), common.Hash{0x4}, common.Hash{0x4}, nil, + }, + /* + lookup account from the bottom + */ + { + common.HexToHash("0x1"), common.Hash{0x3}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0x2"), common.Hash{0x3}, common.Hash{0x3}, nil, + }, + { + common.HexToHash("0x3"), common.Hash{0x3}, common.Hash{0x3}, nil, // not found + }, + /* + stale states + */ + { + common.HexToHash("0x1"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0x2"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0x3"), common.Hash{0x2}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0x1"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0x2"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + { + common.HexToHash("0x3"), common.Hash{0x1}, common.Hash{}, errSnapshotStale, + }, + } + for i, c := range cases2 { + l, err := tr.lookupStorage(common.HexToHash("0xa"), c.storage, c.state) + if c.expectErr != nil { + if !errors.Is(err, c.expectErr) { + t.Fatalf("%d: unexpected error, want %v, got %v", i, c.expectErr, err) + } + } + if c.expectErr == nil { + if err != nil { + t.Fatalf("%d: %v", i, err) + } + if l.rootHash() != c.expect { + t.Errorf("Unexpected tiphash, %d, want: %x, got: %x", i, c.expect, l.rootHash()) + } + } + } +} diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go new file mode 100644 index 000000000000..a43621db77dd --- /dev/null +++ b/triedb/pathdb/lookup.go @@ -0,0 +1,281 @@ +// Copyright 2024 The 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 . + +package pathdb + +import ( + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/sync/errgroup" +) + +// slicePool is a shared pool of hash slice, for reducing the GC pressure. +var slicePool = sync.Pool{ + New: func() interface{} { + slice := make([]common.Hash, 0, 16) // Pre-allocate a slice with a reasonable capacity. + return &slice + }, +} + +// getSlice obtains the hash slice from the shared pool. +func getSlice() []common.Hash { + slice := *slicePool.Get().(*[]common.Hash) + slice = slice[:0] + return slice +} + +// returnSlice returns the hash slice back to the shared pool for following usage. +func returnSlice(slice []common.Hash) { + slicePool.Put(&slice) +} + +// lookup is an internal structure used to efficiently determine the layer in +// which a state entry resides. +type lookup struct { + accounts map[common.Hash][]common.Hash + storages map[common.Hash]map[common.Hash][]common.Hash + descendant func(state common.Hash, ancestor common.Hash) bool +} + +// newLookup initializes the lookup structure. +func newLookup(head layer, descendant func(state common.Hash, ancestor common.Hash) bool) *lookup { + var ( + current = head + layers []layer + ) + for current != nil { + layers = append(layers, current) + current = current.parentLayer() + } + l := &lookup{ + accounts: make(map[common.Hash][]common.Hash), + storages: make(map[common.Hash]map[common.Hash][]common.Hash), + descendant: descendant, + } + // Apply the diff layers from bottom to top + for i := len(layers) - 1; i >= 0; i-- { + switch diff := layers[i].(type) { + case *diskLayer: + continue + case *diffLayer: + l.addLayer(diff) + } + } + return l +} + +// accountTip traverses the layer list associated with the given account in +// reverse order to locate the first entry that either matches the specified +// stateID or is a descendant of it. +// +// If found, the account data corresponding to the supplied stateID resides +// in that layer. Otherwise, two scenarios are possible: +// +// The account remains unmodified from the current disk layer up to the state +// layer specified by the stateID: fallback to the disk layer for data retrieval. +// Or the layer specified by the stateID is stale: reject the data retrieval. +func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { + list := l.accounts[accountHash] + for i := len(list) - 1; i >= 0; i-- { + if list[i] == stateID || l.descendant(stateID, list[i]) { + return list[i] + } + } + // No layer matching the stateID or its descendants was found. Use the + // current disk layer as a fallback. + if base == stateID || l.descendant(stateID, base) { + return base + } + // The layer associated with 'stateID' is not the descendant of the current + // disk layer, it's already stale, return nothing. + return common.Hash{} +} + +// storageTip traverses the layer list associated with the given account and +// slot hash in reverse order to locate the first entry that either matches +// the specified stateID or is a descendant of it. +// +// If found, the storage data corresponding to the supplied stateID resides +// in that layer. Otherwise, two scenarios are possible: +// +// The storage slot remains unmodified from the current disk layer up to the +// state layer specified by the stateID: fallback to the disk layer for data +// retrieval. Or the layer specified by the stateID is stale: reject the data +// retrieval. +func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { + subset, exists := l.storages[accountHash] + if exists { + list := subset[slotHash] + for i := len(list) - 1; i >= 0; i-- { + if list[i] == stateID || l.descendant(stateID, list[i]) { + return list[i] + } + } + } + // No layer matching the stateID or its descendants was found. Use the + // current disk layer as a fallback. + if base == stateID || l.descendant(stateID, base) { + return base + } + // The layer associated with 'stateID' is not the descendant of the current + // disk layer, it's already stale, return nothing. + return common.Hash{} +} + +// addLayer traverses the state data retained in the specified diff layer and +// integrates it into the lookup set. +// +// This function assumes that all layers older than the provided one have already +// been processed, ensuring that layers are processed strictly in a bottom-to-top +// order. +func (l *lookup) addLayer(diff *diffLayer) { + defer func(now time.Time) { + lookupAddLayerTimer.UpdateSince(now) + }(time.Now()) + + var ( + wg sync.WaitGroup + state = diff.rootHash() + ) + wg.Add(1) + go func() { + defer wg.Done() + for accountHash := range diff.states.accountData { + list, exists := l.accounts[accountHash] + if !exists { + list = getSlice() + } + list = append(list, state) + l.accounts[accountHash] = list + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for accountHash, slots := range diff.states.storageData { + subset := l.storages[accountHash] + if subset == nil { + subset = make(map[common.Hash][]common.Hash) + l.storages[accountHash] = subset + } + for slotHash := range slots { + list, exists := subset[slotHash] + if !exists { + list = getSlice() + } + list = append(list, state) + subset[slotHash] = list + } + } + }() + wg.Wait() +} + +// removeLayer traverses the state data retained in the specified diff layer and +// unlink them from the lookup set. +func (l *lookup) removeLayer(diff *diffLayer) error { + defer func(now time.Time) { + lookupRemoveLayerTimer.UpdateSince(now) + }(time.Now()) + + var ( + wg errgroup.Group + state = diff.rootHash() + ) + wg.Go(func() error { + for accountHash := range diff.states.accountData { + var ( + found bool + list = l.accounts[accountHash] + ) + // Traverse the list from oldest to newest to quickly locate the ID + // of the stale layer. + for i := 0; i < len(list); i++ { + if list[i] == state { + if i == 0 { + list = list[1:] + if cap(list) > 1024 { + list = append(getSlice(), list...) + } + } else { + list = append(list[:i], list[i+1:]...) + } + found = true + break + } + } + if !found { + return fmt.Errorf("account lookup is not found, %x, state: %x", accountHash, state) + } + if len(list) != 0 { + l.accounts[accountHash] = list + } else { + returnSlice(list) + delete(l.accounts, accountHash) + } + } + return nil + }) + + wg.Go(func() error { + for accountHash, slots := range diff.states.storageData { + subset := l.storages[accountHash] + if subset == nil { + return fmt.Errorf("storage lookup is not found, %x", accountHash) + } + for slotHash := range slots { + var ( + found bool + list = subset[slotHash] + ) + // Traverse the list from oldest to newest to quickly locate the ID + // of the stale layer. + for i := 0; i < len(list); i++ { + if list[i] == state { + if i == 0 { + list = list[1:] + if cap(list) > 1024 { + list = append(getSlice(), list...) + } + } else { + list = append(list[:i], list[i+1:]...) + } + found = true + break + } + } + if !found { + return fmt.Errorf("storage lookup is not found, %x %x, state: %x", accountHash, slotHash, state) + } + if len(list) != 0 { + subset[slotHash] = list + } else { + returnSlice(subset[slotHash]) + delete(subset, slotHash) + } + } + if len(subset) == 0 { + delete(l.storages, accountHash) + } + } + return nil + }) + return wg.Wait() +} diff --git a/triedb/pathdb/metrics.go b/triedb/pathdb/metrics.go index 502c34fc62ec..6d40c5713b4f 100644 --- a/triedb/pathdb/metrics.go +++ b/triedb/pathdb/metrics.go @@ -72,6 +72,9 @@ var ( historyBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/time", nil) historyDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/data", nil) historyIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/index", nil) + + lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil) + lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil) ) // Metrics in generation diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index 30f75d10582e..9a8b970cc920 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -17,6 +17,7 @@ package pathdb import ( + "errors" "fmt" "github.com/ethereum/go-ethereum/common" @@ -50,8 +51,10 @@ func (loc *nodeLoc) string() string { // reader implements the database.NodeReader interface, providing the functionalities to // retrieve trie nodes by wrapping the internal state layer. type reader struct { - layer layer + db *Database + state common.Hash noHashCheck bool + layer layer } // Node implements database.NodeReader interface, retrieving the node with specified @@ -94,7 +97,18 @@ func (r *reader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, // - the returned account data is not a copy, please don't modify it // - no error will be returned if the requested account is not found in database func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) { - return r.layer.account(hash, 0) + l, err := r.db.tree.lookupAccount(hash, r.state) + if err != nil { + return nil, err + } + // If the located layer is stale, fall back to the slow path to retrieve + // the account data. This is an edge case where the located layer is the + // disk layer, and it becomes stale within a very short time window. + blob, err := l.account(hash, 0) + if errors.Is(err, errSnapshotStale) { + return r.layer.account(hash, 0) + } + return blob, err } // Account directly retrieves the account associated with a particular hash in @@ -105,7 +119,7 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) { // - the returned account object is safe to modify // - no error will be returned if the requested account is not found in database func (r *reader) Account(hash common.Hash) (*types.SlimAccount, error) { - blob, err := r.layer.account(hash, 0) + blob, err := r.AccountRLP(hash) if err != nil { return nil, err } @@ -127,7 +141,18 @@ func (r *reader) Account(hash common.Hash) (*types.SlimAccount, error) { // - the returned storage data is not a copy, please don't modify it // - no error will be returned if the requested slot is not found in database func (r *reader) Storage(accountHash, storageHash common.Hash) ([]byte, error) { - return r.layer.storage(accountHash, storageHash, 0) + l, err := r.db.tree.lookupStorage(accountHash, storageHash, r.state) + if err != nil { + return nil, err + } + // If the located layer is stale, fall back to the slow path to retrieve + // the account data. This is an edge case where the located layer is the + // disk layer, and it becomes stale within a very short time window. + blob, err := l.storage(accountHash, storageHash, 0) + if errors.Is(err, errSnapshotStale) { + return r.layer.storage(accountHash, storageHash, 0) + } + return blob, err } // NodeReader retrieves a layer belonging to the given state root. @@ -136,7 +161,12 @@ func (db *Database) NodeReader(root common.Hash) (database.NodeReader, error) { if layer == nil { return nil, fmt.Errorf("state %#x is not available", root) } - return &reader{layer: layer, noHashCheck: db.isVerkle}, nil + return &reader{ + db: db, + state: root, + noHashCheck: db.isVerkle, + layer: layer, + }, nil } // StateReader returns a reader that allows access to the state data associated @@ -146,5 +176,9 @@ func (db *Database) StateReader(root common.Hash) (database.StateReader, error) if layer == nil { return nil, fmt.Errorf("state %#x is not available", root) } - return &reader{layer: layer}, nil + return &reader{ + db: db, + state: root, + layer: layer, + }, nil } From c02137bc6f795d490086527e48af7b7c3744dbad Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Thu, 20 Feb 2025 10:24:04 +0800 Subject: [PATCH 2/7] triedb/pathdb: remove sync pool --- triedb/pathdb/layertree.go | 24 +++++++++++++-- triedb/pathdb/lookup.go | 60 ++++++++++++++++---------------------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index ef0cd65d6f2b..eb8b30dfe181 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -31,8 +31,17 @@ import ( // thread-safe to use. However, callers need to ensure the thread-safety // of the referenced layer by themselves. type layerTree struct { - base *diskLayer - layers map[common.Hash]layer + base *diskLayer + layers map[common.Hash]layer + + // descendants is a two-dimensional map where the keys represent + // an ancestor state root, and the values are the state roots of + // all its descendants. + // + // For example: r -> [c1, c2, ..., cn], where c1 through cn are + // the descendants of state r. + // + // This map includes all the existing diff layers and the disk layer. descendants map[common.Hash]map[common.Hash]struct{} lookup *lookup lock sync.RWMutex @@ -152,8 +161,13 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6 tree.lock.Lock() defer tree.lock.Unlock() + // Link the given layer into the layer set tree.layers[l.rootHash()] = l + + // Link the given layer into its ancestors (up to the current disk layer) tree.fillAncestors(l) + + // Link the given layer into the state mutation history tree.lookup.addLayer(l) return nil } @@ -185,6 +199,8 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { tree.layers = map[common.Hash]layer{ base.rootHash(): base, } + // Resets the descendants map, since there's only a single disk layer + // with no descendants. tree.descendants = make(map[common.Hash]map[common.Hash]struct{}) tree.lookup = newLookup(base, tree.isDescendant) return nil @@ -227,7 +243,7 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { // ->C2'->C3'->C4' // // After change: - // Chain: + // Chain: // (a) C3->C4 (HEAD) // (b) C1->C2 // ->C2'->C3'->C4' @@ -291,6 +307,7 @@ func (tree *layerTree) bottom() *diskLayer { // lookupAccount returns the layer that is confirmed to contain the account data // being searched for. func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) (layer, error) { + // Hold the read lock to prevent the unexpected layer changes tree.lock.RLock() defer tree.lock.RUnlock() @@ -308,6 +325,7 @@ func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) // lookupStorage returns the layer that is confirmed to contain the storage slot // data being searched for. func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Hash, state common.Hash) (layer, error) { + // Hold the read lock to prevent the unexpected layer changes tree.lock.RLock() defer tree.lock.RUnlock() diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index a43621db77dd..5326989f476f 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -25,31 +25,23 @@ import ( "golang.org/x/sync/errgroup" ) -// slicePool is a shared pool of hash slice, for reducing the GC pressure. -var slicePool = sync.Pool{ - New: func() interface{} { - slice := make([]common.Hash, 0, 16) // Pre-allocate a slice with a reasonable capacity. - return &slice - }, -} - -// getSlice obtains the hash slice from the shared pool. -func getSlice() []common.Hash { - slice := *slicePool.Get().(*[]common.Hash) - slice = slice[:0] - return slice -} - -// returnSlice returns the hash slice back to the shared pool for following usage. -func returnSlice(slice []common.Hash) { - slicePool.Put(&slice) -} - // lookup is an internal structure used to efficiently determine the layer in // which a state entry resides. type lookup struct { - accounts map[common.Hash][]common.Hash - storages map[common.Hash]map[common.Hash][]common.Hash + // accounts represents the mutation history for specific accounts. + // The key is the account address hash, and the value is a slice + // of **diff layer** IDs indicating where the account was modified, + // with the order from oldest to newest. + accounts map[common.Hash][]common.Hash + + // storages represents the mutation history for specific storage + // slot. The key is the account address hash and the storage key + // hash, the value is a slice of **diff layer** IDs indicating + // where the slot was modified, with the order from oldest to newest. + storages map[common.Hash]map[common.Hash][]common.Hash + + // descendant is the callback indicating whether the layer with + // given root is a descendant of the one specified by `ancestor`. descendant func(state common.Hash, ancestor common.Hash) bool } @@ -87,9 +79,9 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha // If found, the account data corresponding to the supplied stateID resides // in that layer. Otherwise, two scenarios are possible: // -// The account remains unmodified from the current disk layer up to the state -// layer specified by the stateID: fallback to the disk layer for data retrieval. -// Or the layer specified by the stateID is stale: reject the data retrieval. +// (a) the account remains unmodified from the current disk layer up to the state +// layer specified by the stateID: fallback to the disk layer for data retrieval, +// (b) or the layer specified by the stateID is stale: reject the data retrieval. func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { list := l.accounts[accountHash] for i := len(list) - 1; i >= 0; i-- { @@ -114,10 +106,10 @@ func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base c // If found, the storage data corresponding to the supplied stateID resides // in that layer. Otherwise, two scenarios are possible: // -// The storage slot remains unmodified from the current disk layer up to the -// state layer specified by the stateID: fallback to the disk layer for data -// retrieval. Or the layer specified by the stateID is stale: reject the data -// retrieval. +// (a) the storage slot remains unmodified from the current disk layer up to +// the state layer specified by the stateID: fallback to the disk layer for +// data retrieval, (b) or the layer specified by the stateID is stale: reject +// the data retrieval. func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { subset, exists := l.storages[accountHash] if exists { @@ -159,7 +151,7 @@ func (l *lookup) addLayer(diff *diffLayer) { for accountHash := range diff.states.accountData { list, exists := l.accounts[accountHash] if !exists { - list = getSlice() + list = make([]common.Hash, 0, 16) // TODO(rjl493456442) use sync pool } list = append(list, state) l.accounts[accountHash] = list @@ -178,7 +170,7 @@ func (l *lookup) addLayer(diff *diffLayer) { for slotHash := range slots { list, exists := subset[slotHash] if !exists { - list = getSlice() + list = make([]common.Hash, 0, 16) // TODO(rjl493456442) use sync pool } list = append(list, state) subset[slotHash] = list @@ -212,7 +204,7 @@ func (l *lookup) removeLayer(diff *diffLayer) error { if i == 0 { list = list[1:] if cap(list) > 1024 { - list = append(getSlice(), list...) + list = append(make([]common.Hash, 0, len(list)), list...) } } else { list = append(list[:i], list[i+1:]...) @@ -227,7 +219,6 @@ func (l *lookup) removeLayer(diff *diffLayer) error { if len(list) != 0 { l.accounts[accountHash] = list } else { - returnSlice(list) delete(l.accounts, accountHash) } } @@ -252,7 +243,7 @@ func (l *lookup) removeLayer(diff *diffLayer) error { if i == 0 { list = list[1:] if cap(list) > 1024 { - list = append(getSlice(), list...) + list = append(make([]common.Hash, 0, len(list)), list...) } } else { list = append(list[:i], list[i+1:]...) @@ -267,7 +258,6 @@ func (l *lookup) removeLayer(diff *diffLayer) error { if len(list) != 0 { subset[slotHash] = list } else { - returnSlice(subset[slotHash]) delete(subset, slotHash) } } From 602890ba439c8ae5cd046765ae356a9a98fdf6fc Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Mon, 19 May 2025 09:32:14 +0800 Subject: [PATCH 3/7] triedb/pathdb: update func description --- triedb/pathdb/layertree.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index eb8b30dfe181..b2f3f7f37ded 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -304,8 +304,8 @@ func (tree *layerTree) bottom() *diskLayer { return tree.base } -// lookupAccount returns the layer that is confirmed to contain the account data -// being searched for. +// lookupAccount returns the layer that is guaranteed to contain the account data +// corresponding to the specified state root being queried. func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) (layer, error) { // Hold the read lock to prevent the unexpected layer changes tree.lock.RLock() @@ -322,8 +322,8 @@ func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) return l, nil } -// lookupStorage returns the layer that is confirmed to contain the storage slot -// data being searched for. +// lookupStorage returns the layer that is guaranteed to contain the storage slot +// data corresponding to the specified state root being queried. func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Hash, state common.Hash) (layer, error) { // Hold the read lock to prevent the unexpected layer changes tree.lock.RLock() From 8f9c277fe96227d95eefb5219fda4ab2f39cdb3c Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 27 May 2025 11:45:49 +0800 Subject: [PATCH 4/7] triedb/pathdb: update --- triedb/pathdb/lookup.go | 8 ++++++++ triedb/pathdb/reader.go | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index 5326989f476f..d4ec6493c6f9 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -202,6 +202,10 @@ func (l *lookup) removeLayer(diff *diffLayer) error { for i := 0; i < len(list); i++ { if list[i] == state { if i == 0 { + // Remove the first element by shifting the slice forward. + // Pros: zero-copy. + // Cons: may retain large backing array, causing memory leaks. + // Mitigation: release the array if capacity exceeds threshold. list = list[1:] if cap(list) > 1024 { list = append(make([]common.Hash, 0, len(list)), list...) @@ -241,6 +245,10 @@ func (l *lookup) removeLayer(diff *diffLayer) error { for i := 0; i < len(list); i++ { if list[i] == state { if i == 0 { + // Remove the first element by shifting the slice forward. + // Pros: zero-copy. + // Cons: may retain large backing array, causing memory leaks. + // Mitigation: release the array if capacity exceeds threshold. list = list[1:] if cap(list) > 1024 { list = append(make([]common.Hash, 0, len(list)), list...) diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index 9a8b970cc920..bc72db34e34c 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -103,7 +103,12 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) { } // If the located layer is stale, fall back to the slow path to retrieve // the account data. This is an edge case where the located layer is the - // disk layer, and it becomes stale within a very short time window. + // disk layer (e.g., the requested account was not changed in all the diff + // layers), and it becomes stale within a very short time window. + // + // This fallback mechanism is essential, because the traversal starts from + // the entry point layer and goes down, the staleness of the disk layer does + // not affect the result unless the entry point layer is also stale. blob, err := l.account(hash, 0) if errors.Is(err, errSnapshotStale) { return r.layer.account(hash, 0) @@ -146,8 +151,13 @@ func (r *reader) Storage(accountHash, storageHash common.Hash) ([]byte, error) { return nil, err } // If the located layer is stale, fall back to the slow path to retrieve - // the account data. This is an edge case where the located layer is the - // disk layer, and it becomes stale within a very short time window. + // the storage data. This is an edge case where the located layer is the + // disk layer (e.g., the requested account was not changed in all the diff + // layers), and it becomes stale within a very short time window. + // + // This fallback mechanism is essential, because the traversal starts from + // the entry point layer and goes down, the staleness of the disk layer does + // not affect the result unless the entry point layer is also stale. blob, err := l.storage(accountHash, storageHash, 0) if errors.Is(err, errSnapshotStale) { return r.layer.storage(accountHash, storageHash, 0) From 79617eea5b4e687c4f86f4c9e4abd4b00d020dd8 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 27 May 2025 13:52:19 +0800 Subject: [PATCH 5/7] triedb/pathdb: update comment --- triedb/pathdb/lookup.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index d4ec6493c6f9..ec36ebb04de1 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -83,8 +83,30 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha // layer specified by the stateID: fallback to the disk layer for data retrieval, // (b) or the layer specified by the stateID is stale: reject the data retrieval. func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { + // Traverse the mutation history from latest to oldest one. Several + // scenarios are possible: + // + // Chain: + // D->C1->C2->C3->C4 (HEAD) + // ->C1'->C2'->C3' + // State: + // x: [C1, C1', C3', C3] + // y: [] + // + // - (x, C4) => C3 + // - (x, C3) => C3 + // - (x, C2) => C1 + // - (x, C3') => C3' + // - (x, C2') => C1' + // - (y, C4) => D + // - (y, C3') => D + // - (y, C0) => null list := l.accounts[accountHash] for i := len(list) - 1; i >= 0; i-- { + // If the current state matches the stateID, or the requested state is a + // descendant of it, return the current state as the most recent one + // containing the modified data. Otherwise, the current state may be ahead + // of the requested one or belong to a different branch. if list[i] == stateID || l.descendant(stateID, list[i]) { return list[i] } @@ -115,6 +137,10 @@ func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, state if exists { list := subset[slotHash] for i := len(list) - 1; i >= 0; i-- { + // If the current state matches the stateID, or the requested state is a + // descendant of it, return the current state as the most recent one + // containing the modified data. Otherwise, the current state may be ahead + // of the requested one or belong to a different branch. if list[i] == stateID || l.descendant(stateID, list[i]) { return list[i] } From 285f7898d0d4a91c2491c2baff75e2152905a8c5 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 27 May 2025 21:24:39 +0800 Subject: [PATCH 6/7] triedb/pathdb: define removeFromList function --- triedb/pathdb/lookup.go | 74 +++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index ec36ebb04de1..04fb0f76c725 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -206,6 +206,30 @@ func (l *lookup) addLayer(diff *diffLayer) { wg.Wait() } +// removeFromList removes the specified element from the provided list. +// It returns a flag indicating whether the element was found and removed. +func removeFromList(list []common.Hash, element common.Hash) (bool, []common.Hash) { + // Traverse the list from oldest to newest to quickly locate the element. + for i := 0; i < len(list); i++ { + if list[i] == element { + if i != 0 { + list = append(list[:i], list[i+1:]...) + } else { + // Remove the first element by shifting the slice forward. + // Pros: zero-copy. + // Cons: may retain large backing array, causing memory leaks. + // Mitigation: release the array if capacity exceeds threshold. + list = list[1:] + if cap(list) > 1024 { + list = append(make([]common.Hash, 0, len(list)), list...) + } + } + return true, list + } + } + return false, nil +} + // removeLayer traverses the state data retained in the specified diff layer and // unlink them from the lookup set. func (l *lookup) removeLayer(diff *diffLayer) error { @@ -219,30 +243,7 @@ func (l *lookup) removeLayer(diff *diffLayer) error { ) wg.Go(func() error { for accountHash := range diff.states.accountData { - var ( - found bool - list = l.accounts[accountHash] - ) - // Traverse the list from oldest to newest to quickly locate the ID - // of the stale layer. - for i := 0; i < len(list); i++ { - if list[i] == state { - if i == 0 { - // Remove the first element by shifting the slice forward. - // Pros: zero-copy. - // Cons: may retain large backing array, causing memory leaks. - // Mitigation: release the array if capacity exceeds threshold. - list = list[1:] - if cap(list) > 1024 { - list = append(make([]common.Hash, 0, len(list)), list...) - } - } else { - list = append(list[:i], list[i+1:]...) - } - found = true - break - } - } + found, list := removeFromList(l.accounts[accountHash], state) if !found { return fmt.Errorf("account lookup is not found, %x, state: %x", accountHash, state) } @@ -262,30 +263,7 @@ func (l *lookup) removeLayer(diff *diffLayer) error { return fmt.Errorf("storage lookup is not found, %x", accountHash) } for slotHash := range slots { - var ( - found bool - list = subset[slotHash] - ) - // Traverse the list from oldest to newest to quickly locate the ID - // of the stale layer. - for i := 0; i < len(list); i++ { - if list[i] == state { - if i == 0 { - // Remove the first element by shifting the slice forward. - // Pros: zero-copy. - // Cons: may retain large backing array, causing memory leaks. - // Mitigation: release the array if capacity exceeds threshold. - list = list[1:] - if cap(list) > 1024 { - list = append(make([]common.Hash, 0, len(list)), list...) - } - } else { - list = append(list[:i], list[i+1:]...) - } - found = true - break - } - } + found, list := removeFromList(subset[slotHash], state) if !found { return fmt.Errorf("storage lookup is not found, %x %x, state: %x", accountHash, slotHash, state) } From 81ed5630d33b1086f7255c465c2929aced84e7bf Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 28 May 2025 10:01:24 +0800 Subject: [PATCH 7/7] triedb/pathdb: use merged storage key --- triedb/pathdb/lookup.go | 63 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/triedb/pathdb/lookup.go b/triedb/pathdb/lookup.go index 04fb0f76c725..8b092730f8fc 100644 --- a/triedb/pathdb/lookup.go +++ b/triedb/pathdb/lookup.go @@ -25,6 +25,14 @@ import ( "golang.org/x/sync/errgroup" ) +// storageKey returns a key for uniquely identifying the storage slot. +func storageKey(accountHash common.Hash, slotHash common.Hash) [64]byte { + var key [64]byte + copy(key[:32], accountHash[:]) + copy(key[32:], slotHash[:]) + return key +} + // lookup is an internal structure used to efficiently determine the layer in // which a state entry resides. type lookup struct { @@ -38,7 +46,7 @@ type lookup struct { // slot. The key is the account address hash and the storage key // hash, the value is a slice of **diff layer** IDs indicating // where the slot was modified, with the order from oldest to newest. - storages map[common.Hash]map[common.Hash][]common.Hash + storages map[[64]byte][]common.Hash // descendant is the callback indicating whether the layer with // given root is a descendant of the one specified by `ancestor`. @@ -57,7 +65,7 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha } l := &lookup{ accounts: make(map[common.Hash][]common.Hash), - storages: make(map[common.Hash]map[common.Hash][]common.Hash), + storages: make(map[[64]byte][]common.Hash), descendant: descendant, } // Apply the diff layers from bottom to top @@ -133,17 +141,14 @@ func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base c // data retrieval, (b) or the layer specified by the stateID is stale: reject // the data retrieval. func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash { - subset, exists := l.storages[accountHash] - if exists { - list := subset[slotHash] - for i := len(list) - 1; i >= 0; i-- { - // If the current state matches the stateID, or the requested state is a - // descendant of it, return the current state as the most recent one - // containing the modified data. Otherwise, the current state may be ahead - // of the requested one or belong to a different branch. - if list[i] == stateID || l.descendant(stateID, list[i]) { - return list[i] - } + list := l.storages[storageKey(accountHash, slotHash)] + for i := len(list) - 1; i >= 0; i-- { + // If the current state matches the stateID, or the requested state is a + // descendant of it, return the current state as the most recent one + // containing the modified data. Otherwise, the current state may be ahead + // of the requested one or belong to a different branch. + if list[i] == stateID || l.descendant(stateID, list[i]) { + return list[i] } } // No layer matching the stateID or its descendants was found. Use the @@ -188,18 +193,14 @@ func (l *lookup) addLayer(diff *diffLayer) { go func() { defer wg.Done() for accountHash, slots := range diff.states.storageData { - subset := l.storages[accountHash] - if subset == nil { - subset = make(map[common.Hash][]common.Hash) - l.storages[accountHash] = subset - } for slotHash := range slots { - list, exists := subset[slotHash] + key := storageKey(accountHash, slotHash) + list, exists := l.storages[key] if !exists { list = make([]common.Hash, 0, 16) // TODO(rjl493456442) use sync pool } list = append(list, state) - subset[slotHash] = list + l.storages[key] = list } } }() @@ -238,10 +239,10 @@ func (l *lookup) removeLayer(diff *diffLayer) error { }(time.Now()) var ( - wg errgroup.Group + eg errgroup.Group state = diff.rootHash() ) - wg.Go(func() error { + eg.Go(func() error { for accountHash := range diff.states.accountData { found, list := removeFromList(l.accounts[accountHash], state) if !found { @@ -256,28 +257,22 @@ func (l *lookup) removeLayer(diff *diffLayer) error { return nil }) - wg.Go(func() error { + eg.Go(func() error { for accountHash, slots := range diff.states.storageData { - subset := l.storages[accountHash] - if subset == nil { - return fmt.Errorf("storage lookup is not found, %x", accountHash) - } for slotHash := range slots { - found, list := removeFromList(subset[slotHash], state) + key := storageKey(accountHash, slotHash) + found, list := removeFromList(l.storages[key], state) if !found { return fmt.Errorf("storage lookup is not found, %x %x, state: %x", accountHash, slotHash, state) } if len(list) != 0 { - subset[slotHash] = list + l.storages[key] = list } else { - delete(subset, slotHash) + delete(l.storages, key) } } - if len(subset) == 0 { - delete(l.storages, accountHash) - } } return nil }) - return wg.Wait() + return eg.Wait() }