From f54ee58920dd6021fb2edc92899e7d0537ce3ae8 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Thu, 25 Jul 2024 10:41:09 +0800 Subject: [PATCH] core, trie: extend state reader interface with StorageExists --- core/rawdb/accessors_snapshot.go | 7 +++ core/state/database.go | 5 +++ core/state/reader.go | 61 +++++++++++++++++++++++++++ core/state/snapshot/difflayer.go | 31 ++++++++++++++ core/state/snapshot/disklayer.go | 31 ++++++++++++++ core/state/snapshot/disklayer_test.go | 48 +++++++++++++++++++++ core/state/snapshot/snapshot.go | 4 ++ trie/secure_trie.go | 10 +++++ trie/secure_trie_test.go | 33 +++++++++++++++ trie/verkle.go | 11 +++++ 10 files changed, 241 insertions(+) diff --git a/core/rawdb/accessors_snapshot.go b/core/rawdb/accessors_snapshot.go index 5cea581fcd..4d9a8c61aa 100644 --- a/core/rawdb/accessors_snapshot.go +++ b/core/rawdb/accessors_snapshot.go @@ -98,6 +98,13 @@ func ReadStorageSnapshot(db ethdb.KeyValueReader, accountHash, storageHash commo return data } +// HasStorageSnapshot returns a flag indicating whether the requested storage +// slot exists. +func HasStorageSnapshot(db ethdb.KeyValueReader, accountHash, storageHash common.Hash) bool { + exists, _ := db.Has(storageSnapshotKey(accountHash, storageHash)) + return exists +} + // WriteStorageSnapshot stores the snapshot entry of a storage trie leaf. func WriteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash common.Hash, entry []byte) { if err := db.Put(storageSnapshotKey(accountHash, storageHash), entry); err != nil { diff --git a/core/state/database.go b/core/state/database.go index aad2f382a6..ebe6349997 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -95,6 +95,11 @@ type Trie interface { // a trie.MissingNodeError is returned. GetStorage(addr common.Address, key []byte) ([]byte, error) + // StorageExists returns a flag indicating whether the requested storage slot + // is existent in the trie or not. An error should be returned if the trie + // state is internally corrupted. + StorageExists(addr common.Address, key []byte) (bool, error) + // UpdateAccount abstracts an account write to the trie. It encodes the // provided account object with associated algorithm and then updates it // in the trie with provided address. diff --git a/core/state/reader.go b/core/state/reader.go index 8efe72807f..d8675041f5 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -48,6 +48,10 @@ type Reader interface { // - The returned storage slot is safe to modify after the call Storage(addr common.Address, slot common.Hash) (common.Hash, error) + // StorageExists implements Reader, returning a flag indicating whether the + // requested storage slot exists. + StorageExists(addr common.Address, slot common.Hash) (bool, error) + // Copy returns a deep-copied state reader. Copy() Reader } @@ -126,6 +130,14 @@ func (r *stateReader) Storage(addr common.Address, key common.Hash) (common.Hash return value, nil } +// StorageExists implements Reader, returning a flag indicating whether the +// requested storage slot exists. +func (r *stateReader) StorageExists(addr common.Address, key common.Hash) (bool, error) { + addrHash := crypto.HashData(r.buff, addr.Bytes()) + slotHash := crypto.HashData(r.buff, key.Bytes()) + return r.snap.StorageExists(addrHash, slotHash) +} + // Copy implements Reader, returning a deep-copied snap reader. func (r *stateReader) Copy() Reader { return &stateReader{ @@ -230,6 +242,41 @@ func (r *trieReader) Storage(addr common.Address, key common.Hash) (common.Hash, return value, nil } +// StorageExists implements Reader, returning a flag indicating whether the +// requested storage slot exists. An error will be returned if the trie data +// is internally corrupted. +func (r *trieReader) StorageExists(addr common.Address, key common.Hash) (bool, error) { + var ( + tr Trie + found bool + ) + if r.db.IsVerkle() { + tr = r.mainTrie + } else { + tr, found = r.subTries[addr] + if !found { + root, ok := r.subRoots[addr] + + // The storage slot is accessed without account caching. It's unexpected + // behavior but try to resolve the account first anyway. + if !ok { + _, err := r.Account(addr) + if err != nil { + return false, err + } + root = r.subRoots[addr] + } + var err error + tr, err = trie.NewStateTrie(trie.StorageTrieID(r.root, crypto.HashData(r.buff, addr.Bytes()), root), r.db) + if err != nil { + return false, err + } + r.subTries[addr] = tr + } + } + return tr.StorageExists(addr, key.Bytes()) +} + // Copy implements Reader, returning a deep-copied trie reader. func (r *trieReader) Copy() Reader { tries := make(map[common.Address]Trie) @@ -301,6 +348,20 @@ func (r *multiReader) Storage(addr common.Address, slot common.Hash) (common.Has return common.Hash{}, errors.Join(errs...) } +// StorageExists implements Reader, returning a flag indicating whether the +// requested storage slot exists. +func (r *multiReader) StorageExists(addr common.Address, slot common.Hash) (bool, error) { + var errs []error + for _, reader := range r.readers { + exists, err := reader.StorageExists(addr, slot) + if err == nil { + return exists, nil + } + errs = append(errs, err) + } + return false, errors.Join(errs...) +} + // Copy implementing Reader interface, returning a deep-copied state reader. func (r *multiReader) Copy() Reader { var readers []Reader diff --git a/core/state/snapshot/difflayer.go b/core/state/snapshot/difflayer.go index 779c1ea98c..7e62f37726 100644 --- a/core/state/snapshot/difflayer.go +++ b/core/state/snapshot/difflayer.go @@ -365,6 +365,37 @@ func (dl *diffLayer) Storage(accountHash, storageHash common.Hash) ([]byte, erro return dl.storage(accountHash, storageHash, 0) } +// StorageExists returns a flag indicating whether the requested storage slot is +// existent or not. +func (dl *diffLayer) StorageExists(accountHash, storageHash common.Hash) (bool, error) { + // Check the bloom filter first whether there's even a point in reaching into + // all the maps in all the layers below + dl.lock.RLock() + // Check staleness before reaching further. + if dl.Stale() { + dl.lock.RUnlock() + return false, ErrSnapshotStale + } + hit := dl.diffed.ContainsHash(storageBloomHash(accountHash, storageHash)) + if !hit { + hit = dl.diffed.ContainsHash(destructBloomHash(accountHash)) + } + var origin *diskLayer + if !hit { + origin = dl.origin // extract origin while holding the lock + } + dl.lock.RUnlock() + + // If the bloom filter misses, don't even bother with traversing the memory + // diff layers, reach straight into the bottom persistent disk layer + if origin != nil { + snapshotBloomStorageMissMeter.Mark(1) + return origin.StorageExists(accountHash, storageHash) + } + // The bloom filter hit, start poking in the internal maps + return dl.StorageExists(accountHash, storageHash) +} + // storage is an internal version of Storage that skips the bloom filter checks // and uses the internal maps to try and retrieve the data. It's meant to be // used if a higher layer's bloom filter hit already. diff --git a/core/state/snapshot/disklayer.go b/core/state/snapshot/disklayer.go index f5518a204c..e1aaef367a 100644 --- a/core/state/snapshot/disklayer.go +++ b/core/state/snapshot/disklayer.go @@ -169,6 +169,37 @@ func (dl *diskLayer) Storage(accountHash, storageHash common.Hash) ([]byte, erro return blob, nil } +// StorageExists returns a flag indicating whether the requested storage slot is +// existent or not. +func (dl *diskLayer) StorageExists(accountHash, storageHash common.Hash) (bool, error) { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // If the layer was flattened into, consider it invalid (any live reference to + // the original should be marked as unusable). + if dl.stale { + return false, ErrSnapshotStale + } + key := append(accountHash[:], storageHash[:]...) + + // If the layer is being generated, ensure the requested hash has already been + // covered by the generator. + if dl.genMarker != nil && bytes.Compare(key, dl.genMarker) > 0 { + return false, ErrNotCoveredYet + } + // If we're in the disk layer, all diff layers missed + snapshotDirtyStorageMissMeter.Mark(1) + + // Try to retrieve the storage slot from the memory cache + if blob, found := dl.cache.HasGet(nil, key); found { + snapshotCleanStorageHitMeter.Mark(1) + snapshotCleanStorageReadMeter.Mark(int64(len(blob))) + return true, nil + } + snapshotCleanStorageMissMeter.Mark(1) + return rawdb.HasStorageSnapshot(dl.diskdb, accountHash, storageHash), nil +} + // Update creates a new layer on top of the existing snapshot diff tree with // the specified data items. Note, the maps are retained by the method to avoid // copying everything. diff --git a/core/state/snapshot/disklayer_test.go b/core/state/snapshot/disklayer_test.go index 168458c405..5623e51b66 100644 --- a/core/state/snapshot/disklayer_test.go +++ b/core/state/snapshot/disklayer_test.go @@ -572,3 +572,51 @@ func TestDiskSeek(t *testing.T) { } } } + +func TestDiskStorageExists(t *testing.T) { + // Create some accounts in the disk layer + db := rawdb.NewMemoryDatabase() + defer db.Close() + + // fill storage slot with zero size + rawdb.WriteStorageSnapshot(db, common.Hash{0x1}, common.Hash{0x1}, []byte{}) + rawdb.WriteStorageSnapshot(db, common.Hash{0x1}, common.Hash{0x2}, nil) + rawdb.WriteStorageSnapshot(db, common.Hash{0x1}, common.Hash{0x3}, []byte{0x1}) + + dl := &diskLayer{ + diskdb: db, + cache: fastcache.New(500 * 1024), + root: randomHash(), + } + // Test some different seek positions + type testcase struct { + key common.Hash + expect bool + } + var cases = []testcase{ + { + common.Hash{0x0}, false, + }, + { + common.Hash{0x1}, true, + }, + { + common.Hash{0x2}, true, + }, + { + common.Hash{0x3}, true, + }, + { + common.Hash{0x4}, false, + }, + } + for i, tc := range cases { + result, err := dl.StorageExists(common.Hash{0x1}, tc.key) + if err != nil { + t.Fatalf("Failed to query disk layer: %v", err) + } + if result != tc.expect { + t.Fatalf("%d, unexpected result, want %t, got: %t", i, tc.expect, result) + } + } +} diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index 752f4359fb..638d15ec40 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -112,6 +112,10 @@ type Snapshot interface { // Storage directly retrieves the storage data associated with a particular hash, // within a particular account. Storage(accountHash, storageHash common.Hash) ([]byte, error) + + // StorageExists returns a flag indicating whether the requested storage slot is + // existent or not. + StorageExists(accountHash, storageHash common.Hash) (bool, error) } // snapshot is the internal version of the snapshot data layer that supports some diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 91fd38269f..6bcd10b00a 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -113,6 +113,16 @@ func (t *StateTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) { return content, err } +// StorageExists implements state.Trie, returning a flag indicating whether the +// requested storage slot is existent or not. +func (t *StateTrie) StorageExists(addr common.Address, key []byte) (bool, error) { + data, err := t.GetStorage(addr, key) + if err != nil { + return false, nil + } + return len(data) != 0, nil +} + // GetAccount attempts to retrieve an account with provided account address. // If the specified account is not in the trie, nil will be returned. // If a trie node is not found in the database, a MissingNodeError is returned. diff --git a/trie/secure_trie_test.go b/trie/secure_trie_test.go index 59958d33f4..5d950b5de1 100644 --- a/trie/secure_trie_test.go +++ b/trie/secure_trie_test.go @@ -147,3 +147,36 @@ func TestStateTrieConcurrency(t *testing.T) { // Wait for all threads to finish pend.Wait() } + +func TestSecureStorageExists(t *testing.T) { + trie := newEmptySecure() + + // Zero size value + trie.MustUpdate([]byte("foo"), []byte("")) + exists, err := trie.StorageExists(common.Address{}, []byte("foo")) + if err != nil { + t.Fatalf("trie is corrupted: %v", err) + } + if exists { + t.Fatal("Unexpected trie element") + } + + // Non-existent value + exists, err = trie.StorageExists(common.Address{}, []byte("dead")) + if err != nil { + t.Fatalf("trie is corrupted: %v", err) + } + if exists { + t.Fatal("Unexpected trie element") + } + + // Non-zero size value + trie.MustUpdate([]byte("foo"), []byte("bar")) + exists, err = trie.StorageExists(common.Address{}, []byte("foo")) + if err != nil { + t.Fatalf("trie is corrupted: %v", err) + } + if !exists { + t.Fatal("Trie element is missing") + } +} diff --git a/trie/verkle.go b/trie/verkle.go index 6bd9d3d1af..98ef9af3fb 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -121,6 +121,17 @@ func (t *VerkleTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) return common.TrimLeftZeroes(val), nil } +// StorageExists implements state.Trie, returning a flag indicating whether the +// requested storage slot is existent or not. +func (t *VerkleTrie) StorageExists(addr common.Address, key []byte) (bool, error) { + k := utils.StorageSlotKeyWithEvaluatedAddress(t.cache.Get(addr.Bytes()), key) + val, err := t.root.Get(k, t.nodeResolver) + if err != nil { + return false, err + } + return len(val) != 0, nil +} + // UpdateAccount implements state.Trie, writing the provided account into the tree. // If the tree is corrupted, an error will be returned. func (t *VerkleTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {