Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core/state/snapshot: clean up dangling storages in snapshot generation #24665

Closed
wants to merge 8 commits into from
14 changes: 7 additions & 7 deletions cmd/devp2p/internal/ethtest/snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ func (s *Suite) TestSnapTrieNodes(t *utesting.T) {
{
root: s.chain.RootAt(999),
paths: []snap.TrieNodePathSet{
snap.TrieNodePathSet{}, // zero-length pathset should 'abort' and kick us off
snap.TrieNodePathSet{[]byte{0}},
{}, // zero-length pathset should 'abort' and kick us off
{[]byte{0}},
},
nBytes: 5000,
expHashes: []common.Hash{},
Expand All @@ -382,8 +382,8 @@ func (s *Suite) TestSnapTrieNodes(t *utesting.T) {
{
root: s.chain.RootAt(999),
paths: []snap.TrieNodePathSet{
snap.TrieNodePathSet{[]byte{0}},
snap.TrieNodePathSet{[]byte{1}, []byte{0}},
{[]byte{0}},
{[]byte{1}, []byte{0}},
},
nBytes: 5000,
//0x6b3724a41b8c38b46d4d02fba2bb2074c47a507eb16a9a4b978f91d32e406faf
Expand All @@ -392,7 +392,7 @@ func (s *Suite) TestSnapTrieNodes(t *utesting.T) {
{ // nonsensically long path
root: s.chain.RootAt(999),
paths: []snap.TrieNodePathSet{
snap.TrieNodePathSet{[]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8,
{[]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8}},
},
nBytes: 5000,
Expand All @@ -401,8 +401,8 @@ func (s *Suite) TestSnapTrieNodes(t *utesting.T) {
{
root: s.chain.RootAt(0),
paths: []snap.TrieNodePathSet{
snap.TrieNodePathSet{[]byte{0}},
snap.TrieNodePathSet{[]byte{1}, []byte{0}},
{[]byte{0}},
{[]byte{1}, []byte{0}},
},
nBytes: 5000,
expHashes: []common.Hash{},
Expand Down
73 changes: 11 additions & 62 deletions cmd/geth/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"time"

Expand Down Expand Up @@ -263,10 +262,9 @@ func verifyState(ctx *cli.Context) error {
return err
}
log.Info("Verified the state", "root", root)
if err := checkDangling(chaindb, snaptree.Snapshot(root)); err != nil {
log.Error("Dangling snap storage check failed", "root", root, "err", err)
return err
}

// Detect if there is any dangling storages.
checkDangling(chaindb)
return nil
}

Expand All @@ -276,65 +274,16 @@ func checkDanglingStorage(ctx *cli.Context) error {
stack, _ := makeConfigNode(ctx)
defer stack.Close()

chaindb := utils.MakeChainDatabase(ctx, stack, true)
headBlock := rawdb.ReadHeadBlock(chaindb)
if headBlock == nil {
log.Error("Failed to load head block")
return errors.New("no head block")
}
snaptree, err := snapshot.New(chaindb, trie.NewDatabase(chaindb), 256, headBlock.Root(), false, false, false)
if err != nil {
log.Error("Failed to open snapshot tree", "err", err)
return err
}
if ctx.NArg() > 1 {
log.Error("Too many arguments given")
return errors.New("too many arguments")
}
var root = headBlock.Root()
if ctx.NArg() == 1 {
root, err = parseRoot(ctx.Args()[0])
if err != nil {
log.Error("Failed to resolve state root", "err", err)
return err
}
}
return checkDangling(chaindb, snaptree.Snapshot(root))
checkDangling(utils.MakeChainDatabase(ctx, stack, true))
return nil
}

func checkDangling(chaindb ethdb.Database, snap snapshot.Snapshot) error {
log.Info("Checking dangling snapshot storage")
var (
lastReport = time.Now()
start = time.Now()
lastKey []byte
it = rawdb.NewKeyLengthIterator(chaindb.NewIterator(rawdb.SnapshotStoragePrefix, nil), 1+2*common.HashLength)
)
defer it.Release()
for it.Next() {
k := it.Key()
accKey := k[1:33]
if bytes.Equal(accKey, lastKey) {
// No need to look up for every slot
continue
}
lastKey = common.CopyBytes(accKey)
if time.Since(lastReport) > time.Second*8 {
log.Info("Iterating snap storage", "at", fmt.Sprintf("%#x", accKey), "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
data, err := snap.AccountRLP(common.BytesToHash(accKey))
if err != nil {
log.Error("Error loading snap storage data", "account", fmt.Sprintf("%#x", accKey), "err", err)
return err
}
if len(data) == 0 {
log.Error("Dangling storage - missing account", "account", fmt.Sprintf("%#x", accKey), "storagekey", fmt.Sprintf("%#x", k))
return fmt.Errorf("dangling snapshot storage account %#x", accKey)
}
}
log.Info("Verified the snapshot storage", "root", snap.Root(), "time", common.PrettyDuration(time.Since(start)), "err", it.Error())
return nil
// checkDangling is the internal function for detecting dangling storages.
func checkDangling(chaindb ethdb.Database) {
// Detect dangling storages in disk layer
snapshot.NewDanglingRange(chaindb, nil, nil, true)

// TODO(rjl493456442) Detect dangling storages in diff layers
}

// traverseState is a helper function used for pruning verification.
Expand Down
129 changes: 129 additions & 0 deletions core/state/snapshot/dangling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2022 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 <http://www.gnu.org/licenses/>.

package snapshot

import (
"bytes"
"fmt"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)

// DanglingRange describes the range for detecting dangling storages.
type DanglingRange struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type DanglingRange struct {
type DanglingRangeScanner struct {

perhaps?

db ethdb.KeyValueStore // The database stores the snapshot data
start []byte // The start of the key range
limit []byte // The last of the key range

result []common.Hash // The list of account hashes which have the dangling storages
duration time.Duration // Total time spent on the iteration
}

// NewDanglingRange initializes a dangling storage scanner and detects all the
// dangling accounts out.
func NewDanglingRange(db ethdb.KeyValueStore, start, limit []byte, report bool) *DanglingRange {
r := &DanglingRange{
db: db,
start: start,
limit: limit,
}
r.result, r.duration = r.detect(report)

// Update metrics
snapDanglingStoragesCounter.Inc(int64(len(r.result)))
snapDanglingStoragesTimer.Update(r.duration)

if len(r.result) > 0 {
log.Warn("Detected dangling storages", "number", len(r.result), "start", hexutil.Encode(start), "limit", hexutil.Encode(limit), "elapsed", common.PrettyDuration(r.duration))
} else {
logger := log.Debug
if report {
logger = log.Info
}
logger("Verified snapshot storages", "start", hexutil.Encode(start), "limit", hexutil.Encode(limit), "elapsed", common.PrettyDuration(r.duration))
}
return r
}

// detect iterates the storage snapshot in the specified key range and
// returns a list of account hash of the dangling storages. Note both
// start and limit are included for iteration.
func (r *DanglingRange) detect(report bool) ([]common.Hash, time.Duration) {
var (
checked []byte
result []common.Hash
start = time.Now()
lastReport = time.Now()
)
iter := rawdb.NewKeyLengthIterator(r.db.NewIterator(rawdb.SnapshotStoragePrefix, r.start), len(rawdb.SnapshotStoragePrefix)+2*common.HashLength)
defer iter.Release()

for iter.Next() {
account := iter.Key()[len(rawdb.SnapshotStoragePrefix) : len(rawdb.SnapshotStoragePrefix)+common.HashLength]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that the way you have written it is "more correct", but IMO it's easier to read on the form

account := iter.Key()[1:33] // trim 1-byte prefix, read 32-byte hash

Might be a minority opinion though ...

if r.limit != nil && bytes.Compare(account, r.limit) > 0 {
break
}
// Skip unnecessary checks for checked storage.
if bytes.Equal(account, checked) {
continue
}
checked = common.CopyBytes(account)

// Check the presence of the corresponding account.
accountHash := common.BytesToHash(account)
data := rawdb.ReadAccountSnapshot(r.db, accountHash)
if len(data) != 0 {
continue
}
result = append(result, accountHash)

if report {
log.Warn("Dangling storage - missing account", "account", fmt.Sprintf("%#x", accountHash))
}
if time.Since(lastReport) > time.Second*8 && report {
log.Info("Detecting dangling storage", "at", fmt.Sprintf("%#x", accountHash), "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
}
return result, time.Since(start)
}

// cleanup wipes the dangling storages which fall within the range before the given key.
func (r *DanglingRange) cleanup(limit []byte) error {
var (
err error
wiped int
)
for _, accountHash := range r.result {
if limit != nil && bytes.Compare(accountHash.Bytes(), limit) >= 0 {
break
}
prefix := append(rawdb.SnapshotStoragePrefix, accountHash.Bytes()...)
keylen := len(rawdb.SnapshotStoragePrefix) + 2*common.HashLength
if err = wipeKeyRange(r.db, "storage", prefix, nil, nil, keylen, snapWipedStorageMeter, false); err != nil {
break
}
wiped += 1
}
r.result = r.result[wiped:]
return err
}
Loading