Unverified Commit 7088f1e8 authored by gary rong's avatar gary rong Committed by GitHub

core, eth: faster snapshot generation (#22504)

* eth/protocols: persist received state segments

* core: initial implementation

* core/state/snapshot: add tests

* core, eth: updates

* eth/protocols/snapshot: count flat state size

* core/state: add metrics

* core/state/snapshot: skip unnecessary deletion

* core/state/snapshot: rename

* core/state/snapshot: use the global batch

* core/state/snapshot: add logs and fix wiping

* core/state/snapshot: fix

* core/state/snapshot: save generation progress even if the batch is empty

* core/state/snapshot: fixes

* core/state/snapshot: fix initial account range length

* core/state/snapshot: fix initial account range

* eth/protocols/snap: store flat states during the healing

* eth/protocols/snap: print logs

* core/state/snapshot: refactor (#4)

* core/state/snapshot: refactor

* core/state/snapshot: tiny fix and polish
Co-authored-by: 's avatarrjl493456442 <garyrong0905@gmail.com>

* core, eth: fixes

* core, eth: fix healing writer

* core, trie, eth: fix paths

* eth/protocols/snap: fix encoding

* eth, core: add debug log

* core/state/generate: release iterator asap (#5)

core/state/snapshot: less copy

core/state/snapshot: revert split loop

core/state/snapshot: handle storage becoming empty, improve test robustness

core/state: test modified codehash

core/state/snapshot: polish

* core/state/snapshot: optimize stats counter

* core, eth: add metric

* core/state/snapshot: update comments

* core/state/snapshot: improve tests

* core/state/snapshot: replace secure trie with standard trie

* core/state/snapshot: wrap return as the struct

* core/state/snapshot: skip wiping correct states

* core/state/snapshot: updates

* core/state/snapshot: fixes

* core/state/snapshot: fix panic due to reference flaw in closure

* core/state/snapshot: fix errors in state generation logic + fix log output

* core/state/snapshot: remove an error case

* core/state/snapshot: fix condition-check for exhausted snap state

* core/state/snapshot: use stackTrie for small tries

* core/state/snapshot: don't resolve small storage tries in vain

* core/state/snapshot: properly clean up storage of deleted accounts

* core/state/snapshot: avoid RLP-encoding in some cases + minor nitpicks

* core/state/snapshot: fix error (+testcase)

* core/state/snapshot: clean up tests a bit

* core/state/snapshot: work in progress on better tests

* core/state/snapshot: polish code

* core/state/snapshot: fix trie iteration abortion trigger

* core/state/snapshot: fixes flaws

* core/state/snapshot: remove panic

* core/state/snapshot: fix abort

* core/state/snapshot: more tests (plus failing testcase)

* core/state/snapshot: more testcases + fix for failing test

* core/state/snapshot: testcase for malformed data

* core/state/snapshot: some test nitpicks

* core/state/snapshot: improvements to logging

* core/state/snapshot: testcase to demo error in abortion

* core/state/snapshot: fix abortion

* cmd/geth: make verify-state report the root

* trie: fix failing test

* core/state/snapshot: add timer metrics

* core/state/snapshot: fix metrics

* core/state/snapshot: udpate tests

* eth/protocols/snap: write snapshot account even if code or state is needed

* core/state/snapshot: fix diskmore check

* core/state/snapshot: review fixes

* core/state/snapshot: improve error message

* cmd/geth: rename 'error' to 'err' in logs

* core/state/snapshot: fix some review concerns

* core/state/snapshot, eth/protocols/snap: clear snapshot marker when starting/resuming snap sync

* core: add error log

* core/state/snapshot: use proper timers for metrics collection

* core/state/snapshot: address some review concerns

* eth/protocols/snap: improved log message

* eth/protocols/snap: fix heal logs to condense infos

* core/state/snapshot: wait for generator termination before restarting

* core/state/snapshot: revert timers to counters to track total time
Co-authored-by: 's avatarMartin Holst Swende <martin@swende.se>
Co-authored-by: 's avatarPéter Szilágyi <peterke@gmail.com>
parent a50251e6
...@@ -155,7 +155,7 @@ func pruneState(ctx *cli.Context) error { ...@@ -155,7 +155,7 @@ func pruneState(ctx *cli.Context) error {
chaindb := utils.MakeChainDatabase(ctx, stack, false) chaindb := utils.MakeChainDatabase(ctx, stack, false)
pruner, err := pruner.NewPruner(chaindb, stack.ResolvePath(""), stack.ResolvePath(config.Eth.TrieCleanCacheJournal), ctx.GlobalUint64(utils.BloomFilterSizeFlag.Name)) pruner, err := pruner.NewPruner(chaindb, stack.ResolvePath(""), stack.ResolvePath(config.Eth.TrieCleanCacheJournal), ctx.GlobalUint64(utils.BloomFilterSizeFlag.Name))
if err != nil { if err != nil {
log.Error("Failed to open snapshot tree", "error", err) log.Error("Failed to open snapshot tree", "err", err)
return err return err
} }
if ctx.NArg() > 1 { if ctx.NArg() > 1 {
...@@ -166,12 +166,12 @@ func pruneState(ctx *cli.Context) error { ...@@ -166,12 +166,12 @@ func pruneState(ctx *cli.Context) error {
if ctx.NArg() == 1 { if ctx.NArg() == 1 {
targetRoot, err = parseRoot(ctx.Args()[0]) targetRoot, err = parseRoot(ctx.Args()[0])
if err != nil { if err != nil {
log.Error("Failed to resolve state root", "error", err) log.Error("Failed to resolve state root", "err", err)
return err return err
} }
} }
if err = pruner.Prune(targetRoot); err != nil { if err = pruner.Prune(targetRoot); err != nil {
log.Error("Failed to prune state", "error", err) log.Error("Failed to prune state", "err", err)
return err return err
} }
return nil return nil
...@@ -189,7 +189,7 @@ func verifyState(ctx *cli.Context) error { ...@@ -189,7 +189,7 @@ func verifyState(ctx *cli.Context) error {
} }
snaptree, err := snapshot.New(chaindb, trie.NewDatabase(chaindb), 256, headBlock.Root(), false, false, false) snaptree, err := snapshot.New(chaindb, trie.NewDatabase(chaindb), 256, headBlock.Root(), false, false, false)
if err != nil { if err != nil {
log.Error("Failed to open snapshot tree", "error", err) log.Error("Failed to open snapshot tree", "err", err)
return err return err
} }
if ctx.NArg() > 1 { if ctx.NArg() > 1 {
...@@ -200,15 +200,15 @@ func verifyState(ctx *cli.Context) error { ...@@ -200,15 +200,15 @@ func verifyState(ctx *cli.Context) error {
if ctx.NArg() == 1 { if ctx.NArg() == 1 {
root, err = parseRoot(ctx.Args()[0]) root, err = parseRoot(ctx.Args()[0])
if err != nil { if err != nil {
log.Error("Failed to resolve state root", "error", err) log.Error("Failed to resolve state root", "err", err)
return err return err
} }
} }
if err := snaptree.Verify(root); err != nil { if err := snaptree.Verify(root); err != nil {
log.Error("Failed to verfiy state", "error", err) log.Error("Failed to verfiy state", "root", root, "err", err)
return err return err
} }
log.Info("Verified the state") log.Info("Verified the state", "root", root)
return nil return nil
} }
...@@ -236,7 +236,7 @@ func traverseState(ctx *cli.Context) error { ...@@ -236,7 +236,7 @@ func traverseState(ctx *cli.Context) error {
if ctx.NArg() == 1 { if ctx.NArg() == 1 {
root, err = parseRoot(ctx.Args()[0]) root, err = parseRoot(ctx.Args()[0])
if err != nil { if err != nil {
log.Error("Failed to resolve state root", "error", err) log.Error("Failed to resolve state root", "err", err)
return err return err
} }
log.Info("Start traversing the state", "root", root) log.Info("Start traversing the state", "root", root)
...@@ -247,7 +247,7 @@ func traverseState(ctx *cli.Context) error { ...@@ -247,7 +247,7 @@ func traverseState(ctx *cli.Context) error {
triedb := trie.NewDatabase(chaindb) triedb := trie.NewDatabase(chaindb)
t, err := trie.NewSecure(root, triedb) t, err := trie.NewSecure(root, triedb)
if err != nil { if err != nil {
log.Error("Failed to open trie", "root", root, "error", err) log.Error("Failed to open trie", "root", root, "err", err)
return err return err
} }
var ( var (
...@@ -262,13 +262,13 @@ func traverseState(ctx *cli.Context) error { ...@@ -262,13 +262,13 @@ func traverseState(ctx *cli.Context) error {
accounts += 1 accounts += 1
var acc state.Account var acc state.Account
if err := rlp.DecodeBytes(accIter.Value, &acc); err != nil { if err := rlp.DecodeBytes(accIter.Value, &acc); err != nil {
log.Error("Invalid account encountered during traversal", "error", err) log.Error("Invalid account encountered during traversal", "err", err)
return err return err
} }
if acc.Root != emptyRoot { if acc.Root != emptyRoot {
storageTrie, err := trie.NewSecure(acc.Root, triedb) storageTrie, err := trie.NewSecure(acc.Root, triedb)
if err != nil { if err != nil {
log.Error("Failed to open storage trie", "root", acc.Root, "error", err) log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
return err return err
} }
storageIter := trie.NewIterator(storageTrie.NodeIterator(nil)) storageIter := trie.NewIterator(storageTrie.NodeIterator(nil))
...@@ -276,7 +276,7 @@ func traverseState(ctx *cli.Context) error { ...@@ -276,7 +276,7 @@ func traverseState(ctx *cli.Context) error {
slots += 1 slots += 1
} }
if storageIter.Err != nil { if storageIter.Err != nil {
log.Error("Failed to traverse storage trie", "root", acc.Root, "error", storageIter.Err) log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Err)
return storageIter.Err return storageIter.Err
} }
} }
...@@ -294,7 +294,7 @@ func traverseState(ctx *cli.Context) error { ...@@ -294,7 +294,7 @@ func traverseState(ctx *cli.Context) error {
} }
} }
if accIter.Err != nil { if accIter.Err != nil {
log.Error("Failed to traverse state trie", "root", root, "error", accIter.Err) log.Error("Failed to traverse state trie", "root", root, "err", accIter.Err)
return accIter.Err return accIter.Err
} }
log.Info("State is complete", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) log.Info("State is complete", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start)))
...@@ -326,7 +326,7 @@ func traverseRawState(ctx *cli.Context) error { ...@@ -326,7 +326,7 @@ func traverseRawState(ctx *cli.Context) error {
if ctx.NArg() == 1 { if ctx.NArg() == 1 {
root, err = parseRoot(ctx.Args()[0]) root, err = parseRoot(ctx.Args()[0])
if err != nil { if err != nil {
log.Error("Failed to resolve state root", "error", err) log.Error("Failed to resolve state root", "err", err)
return err return err
} }
log.Info("Start traversing the state", "root", root) log.Info("Start traversing the state", "root", root)
...@@ -337,7 +337,7 @@ func traverseRawState(ctx *cli.Context) error { ...@@ -337,7 +337,7 @@ func traverseRawState(ctx *cli.Context) error {
triedb := trie.NewDatabase(chaindb) triedb := trie.NewDatabase(chaindb)
t, err := trie.NewSecure(root, triedb) t, err := trie.NewSecure(root, triedb)
if err != nil { if err != nil {
log.Error("Failed to open trie", "root", root, "error", err) log.Error("Failed to open trie", "root", root, "err", err)
return err return err
} }
var ( var (
...@@ -368,13 +368,13 @@ func traverseRawState(ctx *cli.Context) error { ...@@ -368,13 +368,13 @@ func traverseRawState(ctx *cli.Context) error {
accounts += 1 accounts += 1
var acc state.Account var acc state.Account
if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil { if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil {
log.Error("Invalid account encountered during traversal", "error", err) log.Error("Invalid account encountered during traversal", "err", err)
return errors.New("invalid account") return errors.New("invalid account")
} }
if acc.Root != emptyRoot { if acc.Root != emptyRoot {
storageTrie, err := trie.NewSecure(acc.Root, triedb) storageTrie, err := trie.NewSecure(acc.Root, triedb)
if err != nil { if err != nil {
log.Error("Failed to open storage trie", "root", acc.Root, "error", err) log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
return errors.New("missing storage trie") return errors.New("missing storage trie")
} }
storageIter := storageTrie.NodeIterator(nil) storageIter := storageTrie.NodeIterator(nil)
...@@ -397,7 +397,7 @@ func traverseRawState(ctx *cli.Context) error { ...@@ -397,7 +397,7 @@ func traverseRawState(ctx *cli.Context) error {
} }
} }
if storageIter.Error() != nil { if storageIter.Error() != nil {
log.Error("Failed to traverse storage trie", "root", acc.Root, "error", storageIter.Error()) log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Error())
return storageIter.Error() return storageIter.Error()
} }
} }
...@@ -416,7 +416,7 @@ func traverseRawState(ctx *cli.Context) error { ...@@ -416,7 +416,7 @@ func traverseRawState(ctx *cli.Context) error {
} }
} }
if accIter.Error() != nil { if accIter.Error() != nil {
log.Error("Failed to traverse state trie", "root", root, "error", accIter.Error()) log.Error("Failed to traverse state trie", "root", root, "err", accIter.Error())
return accIter.Error() return accIter.Error()
} }
log.Info("State is complete", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start))) log.Info("State is complete", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start)))
......
...@@ -322,7 +322,7 @@ func generateTrieRoot(db ethdb.KeyValueWriter, it Iterator, account common.Hash, ...@@ -322,7 +322,7 @@ func generateTrieRoot(db ethdb.KeyValueWriter, it Iterator, account common.Hash,
return return
} }
if !bytes.Equal(account.Root, subroot.Bytes()) { if !bytes.Equal(account.Root, subroot.Bytes()) {
results <- fmt.Errorf("invalid subroot(%x), want %x, got %x", it.Hash(), account.Root, subroot) results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot)
return return
} }
results <- nil results <- nil
......
This diff is collapsed.
This diff is collapsed.
...@@ -37,7 +37,10 @@ const journalVersion uint64 = 0 ...@@ -37,7 +37,10 @@ const journalVersion uint64 = 0
// journalGenerator is a disk layer entry containing the generator progress marker. // journalGenerator is a disk layer entry containing the generator progress marker.
type journalGenerator struct { type journalGenerator struct {
Wiping bool // Whether the database was in progress of being wiped // Indicator that whether the database was in progress of being wiped.
// It's deprecated but keep it here for background compatibility.
Wiping bool
Done bool // Whether the generator finished creating the snapshot Done bool // Whether the generator finished creating the snapshot
Marker []byte Marker []byte
Accounts uint64 Accounts uint64
...@@ -193,14 +196,6 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int, ...@@ -193,14 +196,6 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int,
} }
// Everything loaded correctly, resume any suspended operations // Everything loaded correctly, resume any suspended operations
if !generator.Done { if !generator.Done {
// If the generator was still wiping, restart one from scratch (fine for
// now as it's rare and the wiper deletes the stuff it touches anyway, so
// restarting won't incur a lot of extra database hops.
var wiper chan struct{}
if generator.Wiping {
log.Info("Resuming previous snapshot wipe")
wiper = wipeSnapshot(diskdb, false)
}
// Whether or not wiping was in progress, load any generator progress too // Whether or not wiping was in progress, load any generator progress too
base.genMarker = generator.Marker base.genMarker = generator.Marker
if base.genMarker == nil { if base.genMarker == nil {
...@@ -214,7 +209,6 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int, ...@@ -214,7 +209,6 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int,
origin = binary.BigEndian.Uint64(generator.Marker) origin = binary.BigEndian.Uint64(generator.Marker)
} }
go base.generate(&generatorStats{ go base.generate(&generatorStats{
wiping: wiper,
origin: origin, origin: origin,
start: time.Now(), start: time.Now(),
accounts: generator.Accounts, accounts: generator.Accounts,
...@@ -381,7 +375,6 @@ func (dl *diskLayer) LegacyJournal(buffer *bytes.Buffer) (common.Hash, error) { ...@@ -381,7 +375,6 @@ func (dl *diskLayer) LegacyJournal(buffer *bytes.Buffer) (common.Hash, error) {
Marker: dl.genMarker, Marker: dl.genMarker,
} }
if stats != nil { if stats != nil {
entry.Wiping = (stats.wiping != nil)
entry.Accounts = stats.accounts entry.Accounts = stats.accounts
entry.Slots = stats.slots entry.Slots = stats.slots
entry.Storage = uint64(stats.storage) entry.Storage = uint64(stats.storage)
......
...@@ -656,9 +656,6 @@ func (t *Tree) Rebuild(root common.Hash) { ...@@ -656,9 +656,6 @@ func (t *Tree) Rebuild(root common.Hash) {
// building a brand new snapshot. // building a brand new snapshot.
rawdb.DeleteSnapshotRecoveryNumber(t.diskdb) rawdb.DeleteSnapshotRecoveryNumber(t.diskdb)
// Track whether there's a wipe currently running and keep it alive if so
var wiper chan struct{}
// Iterate over and mark all layers stale // Iterate over and mark all layers stale
for _, layer := range t.layers { for _, layer := range t.layers {
switch layer := layer.(type) { switch layer := layer.(type) {
...@@ -667,10 +664,7 @@ func (t *Tree) Rebuild(root common.Hash) { ...@@ -667,10 +664,7 @@ func (t *Tree) Rebuild(root common.Hash) {
if layer.genAbort != nil { if layer.genAbort != nil {
abort := make(chan *generatorStats) abort := make(chan *generatorStats)
layer.genAbort <- abort layer.genAbort <- abort
<-abort
if stats := <-abort; stats != nil {
wiper = stats.wiping
}
} }
// Layer should be inactive now, mark it as stale // Layer should be inactive now, mark it as stale
layer.lock.Lock() layer.lock.Lock()
...@@ -691,7 +685,7 @@ func (t *Tree) Rebuild(root common.Hash) { ...@@ -691,7 +685,7 @@ func (t *Tree) Rebuild(root common.Hash) {
// generator will run a wiper first if there's not one running right now. // generator will run a wiper first if there's not one running right now.
log.Info("Rebuilding state snapshot") log.Info("Rebuilding state snapshot")
t.layers = map[common.Hash]snapshot{ t.layers = map[common.Hash]snapshot{
root: generateSnapshot(t.diskdb, t.triedb, t.cache, root, wiper), root: generateSnapshot(t.diskdb, t.triedb, t.cache, root),
} }
} }
......
...@@ -24,10 +24,11 @@ import ( ...@@ -24,10 +24,11 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
) )
// wipeSnapshot starts a goroutine to iterate over the entire key-value database // wipeSnapshot starts a goroutine to iterate over the entire key-value database
// and delete all the data associated with the snapshot (accounts, storage, // and delete all the data associated with the snapshot (accounts, storage,
// metadata). After all is done, the snapshot range of the database is compacted // metadata). After all is done, the snapshot range of the database is compacted
// to free up unused data blocks. // to free up unused data blocks.
func wipeSnapshot(db ethdb.KeyValueStore, full bool) chan struct{} { func wipeSnapshot(db ethdb.KeyValueStore, full bool) chan struct{} {
...@@ -53,10 +54,10 @@ func wipeSnapshot(db ethdb.KeyValueStore, full bool) chan struct{} { ...@@ -53,10 +54,10 @@ func wipeSnapshot(db ethdb.KeyValueStore, full bool) chan struct{} {
// removed in sync to avoid data races. After all is done, the snapshot range of // removed in sync to avoid data races. After all is done, the snapshot range of
// the database is compacted to free up unused data blocks. // the database is compacted to free up unused data blocks.
func wipeContent(db ethdb.KeyValueStore) error { func wipeContent(db ethdb.KeyValueStore) error {
if err := wipeKeyRange(db, "accounts", rawdb.SnapshotAccountPrefix, len(rawdb.SnapshotAccountPrefix)+common.HashLength); err != nil { if err := wipeKeyRange(db, "accounts", rawdb.SnapshotAccountPrefix, nil, nil, len(rawdb.SnapshotAccountPrefix)+common.HashLength, snapWipedAccountMeter, true); err != nil {
return err return err
} }
if err := wipeKeyRange(db, "storage", rawdb.SnapshotStoragePrefix, len(rawdb.SnapshotStoragePrefix)+2*common.HashLength); err != nil { if err := wipeKeyRange(db, "storage", rawdb.SnapshotStoragePrefix, nil, nil, len(rawdb.SnapshotStoragePrefix)+2*common.HashLength, snapWipedStorageMeter, true); err != nil {
return err return err
} }
// Compact the snapshot section of the database to get rid of unused space // Compact the snapshot section of the database to get rid of unused space
...@@ -82,8 +83,11 @@ func wipeContent(db ethdb.KeyValueStore) error { ...@@ -82,8 +83,11 @@ func wipeContent(db ethdb.KeyValueStore) error {
} }
// wipeKeyRange deletes a range of keys from the database starting with prefix // wipeKeyRange deletes a range of keys from the database starting with prefix
// and having a specific total key length. // and having a specific total key length. The start and limit is optional for
func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int) error { // specifying a particular key range for deletion.
//
// Origin is included for wiping and limit is excluded if they are specified.
func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, origin []byte, limit []byte, keylen int, meter metrics.Meter, report bool) error {
// Batch deletions together to avoid holding an iterator for too long // Batch deletions together to avoid holding an iterator for too long
var ( var (
batch = db.NewBatch() batch = db.NewBatch()
...@@ -92,7 +96,11 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int ...@@ -92,7 +96,11 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int
// Iterate over the key-range and delete all of them // Iterate over the key-range and delete all of them
start, logged := time.Now(), time.Now() start, logged := time.Now(), time.Now()
it := db.NewIterator(prefix, nil) it := db.NewIterator(prefix, origin)
var stop []byte
if limit != nil {
stop = append(prefix, limit...)
}
for it.Next() { for it.Next() {
// Skip any keys with the correct prefix but wrong length (trie nodes) // Skip any keys with the correct prefix but wrong length (trie nodes)
key := it.Key() key := it.Key()
...@@ -102,6 +110,9 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int ...@@ -102,6 +110,9 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int
if len(key) != keylen { if len(key) != keylen {
continue continue
} }
if stop != nil && bytes.Compare(key, stop) >= 0 {
break
}
// Delete the key and periodically recreate the batch and iterator // Delete the key and periodically recreate the batch and iterator
batch.Delete(key) batch.Delete(key)
items++ items++
...@@ -116,7 +127,7 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int ...@@ -116,7 +127,7 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int
seekPos := key[len(prefix):] seekPos := key[len(prefix):]
it = db.NewIterator(prefix, seekPos) it = db.NewIterator(prefix, seekPos)
if time.Since(logged) > 8*time.Second { if time.Since(logged) > 8*time.Second && report {
log.Info("Deleting state snapshot leftovers", "kind", kind, "wiped", items, "elapsed", common.PrettyDuration(time.Since(start))) log.Info("Deleting state snapshot leftovers", "kind", kind, "wiped", items, "elapsed", common.PrettyDuration(time.Since(start)))
logged = time.Now() logged = time.Now()
} }
...@@ -126,6 +137,11 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int ...@@ -126,6 +137,11 @@ func wipeKeyRange(db ethdb.KeyValueStore, kind string, prefix []byte, keylen int
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
return err return err
} }
log.Info("Deleted state snapshot leftovers", "kind", kind, "wiped", items, "elapsed", common.PrettyDuration(time.Since(start))) if meter != nil {
meter.Mark(int64(items))
}
if report {
log.Info("Deleted state snapshot leftovers", "kind", kind, "wiped", items, "elapsed", common.PrettyDuration(time.Since(start)))
}
return nil return nil
} }
...@@ -948,7 +948,7 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { ...@@ -948,7 +948,7 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// The onleaf func is called _serially_, so we can reuse the same account // The onleaf func is called _serially_, so we can reuse the same account
// for unmarshalling every time. // for unmarshalling every time.
var account Account var account Account
root, err := s.trie.Commit(func(path []byte, leaf []byte, parent common.Hash) error { root, err := s.trie.Commit(func(_ [][]byte, _ []byte, leaf []byte, parent common.Hash) error {
if err := rlp.DecodeBytes(leaf, &account); err != nil { if err := rlp.DecodeBytes(leaf, &account); err != nil {
return nil return nil
} }
......
...@@ -26,17 +26,31 @@ import ( ...@@ -26,17 +26,31 @@ import (
) )
// NewStateSync create a new state trie download scheduler. // NewStateSync create a new state trie download scheduler.
func NewStateSync(root common.Hash, database ethdb.KeyValueReader, bloom *trie.SyncBloom) *trie.Sync { func NewStateSync(root common.Hash, database ethdb.KeyValueReader, bloom *trie.SyncBloom, onLeaf func(paths [][]byte, leaf []byte) error) *trie.Sync {
// Register the storage slot callback if the external callback is specified.
var onSlot func(paths [][]byte, hexpath []byte, leaf []byte, parent common.Hash) error
if onLeaf != nil {
onSlot = func(paths [][]byte, hexpath []byte, leaf []byte, parent common.Hash) error {
return onLeaf(paths, leaf)
}
}
// Register the account callback to connect the state trie and the storage
// trie belongs to the contract.
var syncer *trie.Sync var syncer *trie.Sync
callback := func(path []byte, leaf []byte, parent common.Hash) error { onAccount := func(paths [][]byte, hexpath []byte, leaf []byte, parent common.Hash) error {
if onLeaf != nil {
if err := onLeaf(paths, leaf); err != nil {
return err
}
}
var obj Account var obj Account
if err := rlp.Decode(bytes.NewReader(leaf), &obj); err != nil { if err := rlp.Decode(bytes.NewReader(leaf), &obj); err != nil {
return err return err
} }
syncer.AddSubTrie(obj.Root, path, parent, nil) syncer.AddSubTrie(obj.Root, hexpath, parent, onSlot)
syncer.AddCodeEntry(common.BytesToHash(obj.CodeHash), path, parent) syncer.AddCodeEntry(common.BytesToHash(obj.CodeHash), hexpath, parent)
return nil return nil
} }
syncer = trie.NewSync(root, database, callback, bloom) syncer = trie.NewSync(root, database, onAccount, bloom)
return syncer return syncer
} }
...@@ -133,7 +133,7 @@ func checkStateConsistency(db ethdb.Database, root common.Hash) error { ...@@ -133,7 +133,7 @@ func checkStateConsistency(db ethdb.Database, root common.Hash) error {
// Tests that an empty state is not scheduled for syncing. // Tests that an empty state is not scheduled for syncing.
func TestEmptyStateSync(t *testing.T) { func TestEmptyStateSync(t *testing.T) {
empty := common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") empty := common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
sync := NewStateSync(empty, rawdb.NewMemoryDatabase(), trie.NewSyncBloom(1, memorydb.New())) sync := NewStateSync(empty, rawdb.NewMemoryDatabase(), trie.NewSyncBloom(1, memorydb.New()), nil)
if nodes, paths, codes := sync.Missing(1); len(nodes) != 0 || len(paths) != 0 || len(codes) != 0 { if nodes, paths, codes := sync.Missing(1); len(nodes) != 0 || len(paths) != 0 || len(codes) != 0 {
t.Errorf(" content requested for empty state: %v, %v, %v", nodes, paths, codes) t.Errorf(" content requested for empty state: %v, %v, %v", nodes, paths, codes)
} }
...@@ -170,7 +170,7 @@ func testIterativeStateSync(t *testing.T, count int, commit bool, bypath bool) { ...@@ -170,7 +170,7 @@ func testIterativeStateSync(t *testing.T, count int, commit bool, bypath bool) {
// Create a destination state and sync with the scheduler // Create a destination state and sync with the scheduler
dstDb := rawdb.NewMemoryDatabase() dstDb := rawdb.NewMemoryDatabase()
sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb), nil)
nodes, paths, codes := sched.Missing(count) nodes, paths, codes := sched.Missing(count)
var ( var (
...@@ -249,7 +249,7 @@ func TestIterativeDelayedStateSync(t *testing.T) { ...@@ -249,7 +249,7 @@ func TestIterativeDelayedStateSync(t *testing.T) {
// Create a destination state and sync with the scheduler // Create a destination state and sync with the scheduler
dstDb := rawdb.NewMemoryDatabase() dstDb := rawdb.NewMemoryDatabase()
sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb), nil)
nodes, _, codes := sched.Missing(0) nodes, _, codes := sched.Missing(0)
queue := append(append([]common.Hash{}, nodes...), codes...) queue := append(append([]common.Hash{}, nodes...), codes...)
...@@ -297,7 +297,7 @@ func testIterativeRandomStateSync(t *testing.T, count int) { ...@@ -297,7 +297,7 @@ func testIterativeRandomStateSync(t *testing.T, count int) {
// Create a destination state and sync with the scheduler // Create a destination state and sync with the scheduler
dstDb := rawdb.NewMemoryDatabase() dstDb := rawdb.NewMemoryDatabase()
sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb), nil)
queue := make(map[common.Hash]struct{}) queue := make(map[common.Hash]struct{})
nodes, _, codes := sched.Missing(count) nodes, _, codes := sched.Missing(count)
...@@ -347,7 +347,7 @@ func TestIterativeRandomDelayedStateSync(t *testing.T) { ...@@ -347,7 +347,7 @@ func TestIterativeRandomDelayedStateSync(t *testing.T) {
// Create a destination state and sync with the scheduler // Create a destination state and sync with the scheduler
dstDb := rawdb.NewMemoryDatabase() dstDb := rawdb.NewMemoryDatabase()
sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb), nil)
queue := make(map[common.Hash]struct{}) queue := make(map[common.Hash]struct{})
nodes, _, codes := sched.Missing(0) nodes, _, codes := sched.Missing(0)
...@@ -414,7 +414,7 @@ func TestIncompleteStateSync(t *testing.T) { ...@@ -414,7 +414,7 @@ func TestIncompleteStateSync(t *testing.T) {
// Create a destination state and sync with the scheduler // Create a destination state and sync with the scheduler
dstDb := rawdb.NewMemoryDatabase() dstDb := rawdb.NewMemoryDatabase()
sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb), nil)
var added []common.Hash var added []common.Hash
......
...@@ -298,7 +298,7 @@ func newStateSync(d *Downloader, root common.Hash) *stateSync { ...@@ -298,7 +298,7 @@ func newStateSync(d *Downloader, root common.Hash) *stateSync {
return &stateSync{ return &stateSync{
d: d, d: d,
root: root, root: root,
sched: state.NewStateSync(root, d.stateDB, d.stateBloom), sched: state.NewStateSync(root, d.stateDB, d.stateBloom, nil),
keccak: sha3.NewLegacyKeccak256().(crypto.KeccakState), keccak: sha3.NewLegacyKeccak256().(crypto.KeccakState),
trieTasks: make(map[common.Hash]*trieTask), trieTasks: make(map[common.Hash]*trieTask),
codeTasks: make(map[common.Hash]*codeTask), codeTasks: make(map[common.Hash]*codeTask),
......
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
...@@ -51,7 +52,7 @@ const ( ...@@ -51,7 +52,7 @@ const (
// maxRequestSize is the maximum number of bytes to request from a remote peer. // maxRequestSize is the maximum number of bytes to request from a remote peer.
maxRequestSize = 512 * 1024 maxRequestSize = 512 * 1024
// maxStorageSetRequestCountis th maximum number of contracts to request the // maxStorageSetRequestCount is the maximum number of contracts to request the
// storage of in a single query. If this number is too low, we're not filling // storage of in a single query. If this number is too low, we're not filling
// responses fully and waste round trip times. If it's too high, we're capping // responses fully and waste round trip times. If it's too high, we're capping
// responses and waste bandwidth. // responses and waste bandwidth.
...@@ -435,9 +436,14 @@ type Syncer struct { ...@@ -435,9 +436,14 @@ type Syncer struct {
bytecodeHealDups uint64 // Number of bytecodes already processed bytecodeHealDups uint64 // Number of bytecodes already processed
bytecodeHealNops uint64 // Number of bytecodes not requested bytecodeHealNops uint64 // Number of bytecodes not requested
startTime time.Time // Time instance when snapshot sync started stateWriter ethdb.Batch // Shared batch writer used for persisting raw states
startAcc common.Hash // Account hash where sync started from accountHealed uint64 // Number of accounts downloaded during the healing stage
logTime time.Time // Time instance when status was last reported accountHealedBytes common.StorageSize // Number of raw account bytes persisted to disk during the healing stage
storageHealed uint64 // Number of storage slots downloaded during the healing stage
storageHealedBytes common.StorageSize // Number of raw storage bytes persisted to disk during the healing stage
startTime time.Time // Time instance when snapshot sync started
logTime time.Time // Time instance when status was last reported
pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown
lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, root) lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, root)
...@@ -477,6 +483,7 @@ func NewSyncer(db ethdb.KeyValueStore) *Syncer { ...@@ -477,6 +483,7 @@ func NewSyncer(db ethdb.KeyValueStore) *Syncer {
bytecodeHealReqFails: make(chan *bytecodeHealRequest), bytecodeHealReqFails: make(chan *bytecodeHealRequest),
trienodeHealResps: make(chan *trienodeHealResponse), trienodeHealResps: make(chan *trienodeHealResponse),
bytecodeHealResps: make(chan *bytecodeHealResponse), bytecodeHealResps: make(chan *bytecodeHealResponse),
stateWriter: db.NewBatch(),
} }
} }
...@@ -544,7 +551,7 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { ...@@ -544,7 +551,7 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error {
s.lock.Lock() s.lock.Lock()
s.root = root s.root = root
s.healer = &healTask{ s.healer = &healTask{
scheduler: state.NewStateSync(root, s.db, nil), scheduler: state.NewStateSync(root, s.db, nil, s.onHealState),
trieTasks: make(map[common.Hash]trie.SyncPath), trieTasks: make(map[common.Hash]trie.SyncPath),
codeTasks: make(map[common.Hash]struct{}), codeTasks: make(map[common.Hash]struct{}),
} }
...@@ -560,6 +567,11 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { ...@@ -560,6 +567,11 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error {
log.Debug("Snapshot sync already completed") log.Debug("Snapshot sync already completed")
return nil return nil
} }
// If sync is still not finished, we need to ensure that any marker is wiped.
// Otherwise, it may happen that requests for e.g. genesis-data is delivered
// from the snapshot data, instead of from the trie
snapshot.ClearSnapshotMarker(s.db)
defer func() { // Persist any progress, independent of failure defer func() { // Persist any progress, independent of failure
for _, task := range s.tasks { for _, task := range s.tasks {
s.forwardAccountTask(task) s.forwardAccountTask(task)
...@@ -569,6 +581,14 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { ...@@ -569,6 +581,14 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error {
}() }()
log.Debug("Starting snapshot sync cycle", "root", root) log.Debug("Starting snapshot sync cycle", "root", root)
// Flush out the last committed raw states
defer func() {
if s.stateWriter.ValueSize() > 0 {
s.stateWriter.Write()
s.stateWriter.Reset()
}
}()
defer s.report(true) defer s.report(true)
// Whether sync completed or not, disregard any future packets // Whether sync completed or not, disregard any future packets
...@@ -1694,7 +1714,7 @@ func (s *Syncer) processBytecodeResponse(res *bytecodeResponse) { ...@@ -1694,7 +1714,7 @@ func (s *Syncer) processBytecodeResponse(res *bytecodeResponse) {
// processStorageResponse integrates an already validated storage response // processStorageResponse integrates an already validated storage response
// into the account tasks. // into the account tasks.
func (s *Syncer) processStorageResponse(res *storageResponse) { func (s *Syncer) processStorageResponse(res *storageResponse) {
// Switch the suntask from pending to idle // Switch the subtask from pending to idle
if res.subTask != nil { if res.subTask != nil {
res.subTask.req = nil res.subTask.req = nil
} }
...@@ -1826,6 +1846,14 @@ func (s *Syncer) processStorageResponse(res *storageResponse) { ...@@ -1826,6 +1846,14 @@ func (s *Syncer) processStorageResponse(res *storageResponse) {
nodes++ nodes++
} }
it.Release() it.Release()
// Persist the received storage segements. These flat state maybe
// outdated during the sync, but it can be fixed later during the
// snapshot generation.
for j := 0; j < len(res.hashes[i]); j++ {
rawdb.WriteStorageSnapshot(batch, account, res.hashes[i][j], res.slots[i][j])
bytes += common.StorageSize(1 + 2*common.HashLength + len(res.slots[i][j]))
}
} }
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
log.Crit("Failed to persist storage slots", "err", err) log.Crit("Failed to persist storage slots", "err", err)
...@@ -1983,6 +2011,14 @@ func (s *Syncer) forwardAccountTask(task *accountTask) { ...@@ -1983,6 +2011,14 @@ func (s *Syncer) forwardAccountTask(task *accountTask) {
} }
it.Release() it.Release()
// Persist the received account segements. These flat state maybe
// outdated during the sync, but it can be fixed later during the
// snapshot generation.
for i, hash := range res.hashes {
blob := snapshot.SlimAccountRLP(res.accounts[i].Nonce, res.accounts[i].Balance, res.accounts[i].Root, res.accounts[i].CodeHash)
rawdb.WriteAccountSnapshot(batch, hash, blob)
bytes += common.StorageSize(1 + common.HashLength + len(blob))
}
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
log.Crit("Failed to persist accounts", "err", err) log.Crit("Failed to persist accounts", "err", err)
} }
...@@ -2569,6 +2605,33 @@ func (s *Syncer) onHealByteCodes(peer SyncPeer, id uint64, bytecodes [][]byte) e ...@@ -2569,6 +2605,33 @@ func (s *Syncer) onHealByteCodes(peer SyncPeer, id uint64, bytecodes [][]byte) e
return nil return nil
} }
// onHealState is a callback method to invoke when a flat state(account
// or storage slot) is downloded during the healing stage. The flat states
// can be persisted blindly and can be fixed later in the generation stage.
// Note it's not concurrent safe, please handle the concurrent issue outside.
func (s *Syncer) onHealState(paths [][]byte, value []byte) error {
if len(paths) == 1 {
var account state.Account
if err := rlp.DecodeBytes(value, &account); err != nil {
return nil
}
blob := snapshot.SlimAccountRLP(account.Nonce, account.Balance, account.Root, account.CodeHash)
rawdb.WriteAccountSnapshot(s.stateWriter, common.BytesToHash(paths[0]), blob)
s.accountHealed += 1
s.accountHealedBytes += common.StorageSize(1 + common.HashLength + len(blob))
}
if len(paths) == 2 {
rawdb.WriteStorageSnapshot(s.stateWriter, common.BytesToHash(paths[0]), common.BytesToHash(paths[1]), value)
s.storageHealed += 1
s.storageHealedBytes += common.StorageSize(1 + 2*common.HashLength + len(value))
}
if s.stateWriter.ValueSize() > ethdb.IdealBatchSize {
s.stateWriter.Write() // It's fine to ignore the error here
s.stateWriter.Reset()
}
return nil
}
// hashSpace is the total size of the 256 bit hash space for accounts. // hashSpace is the total size of the 256 bit hash space for accounts.
var hashSpace = new(big.Int).Exp(common.Big2, common.Big256, nil) var hashSpace = new(big.Int).Exp(common.Big2, common.Big256, nil)
...@@ -2632,7 +2695,9 @@ func (s *Syncer) reportHealProgress(force bool) { ...@@ -2632,7 +2695,9 @@ func (s *Syncer) reportHealProgress(force bool) {
var ( var (
trienode = fmt.Sprintf("%d@%v", s.trienodeHealSynced, s.trienodeHealBytes.TerminalString()) trienode = fmt.Sprintf("%d@%v", s.trienodeHealSynced, s.trienodeHealBytes.TerminalString())
bytecode = fmt.Sprintf("%d@%v", s.bytecodeHealSynced, s.bytecodeHealBytes.TerminalString()) bytecode = fmt.Sprintf("%d@%v", s.bytecodeHealSynced, s.bytecodeHealBytes.TerminalString())
accounts = fmt.Sprintf("%d@%v", s.accountHealed, s.accountHealedBytes.TerminalString())
storage = fmt.Sprintf("%d@%v", s.storageHealed, s.storageHealedBytes.TerminalString())
) )
log.Info("State heal in progress", "nodes", trienode, "codes", bytecode, log.Info("State heal in progress", "accounts", accounts, "slots", storage,
"pending", s.healer.scheduler.Pending()) "codes", bytecode, "nodes", trienode, "pending", s.healer.scheduler.Pending())
} }
...@@ -220,13 +220,13 @@ func (c *committer) commitLoop(db *Database) { ...@@ -220,13 +220,13 @@ func (c *committer) commitLoop(db *Database) {
switch n := n.(type) { switch n := n.(type) {
case *shortNode: case *shortNode:
if child, ok := n.Val.(valueNode); ok { if child, ok := n.Val.(valueNode); ok {
c.onleaf(nil, child, hash) c.onleaf(nil, nil, child, hash)
} }
case *fullNode: case *fullNode:
// For children in range [0, 15], it's impossible // For children in range [0, 15], it's impossible
// to contain valuenode. Only check the 17th child. // to contain valuenode. Only check the 17th child.
if n.Children[16] != nil { if n.Children[16] != nil {
c.onleaf(nil, n.Children[16].(valueNode), hash) c.onleaf(nil, nil, n.Children[16].(valueNode), hash)
} }
} }
} }
......
...@@ -398,7 +398,14 @@ func (s *Sync) children(req *request, object node) ([]*request, error) { ...@@ -398,7 +398,14 @@ func (s *Sync) children(req *request, object node) ([]*request, error) {
// Notify any external watcher of a new key/value node // Notify any external watcher of a new key/value node
if req.callback != nil { if req.callback != nil {
if node, ok := (child.node).(valueNode); ok { if node, ok := (child.node).(valueNode); ok {
if err := req.callback(child.path, node, req.hash); err != nil { var paths [][]byte
if len(child.path) == 2*common.HashLength {
paths = append(paths, hexToKeybytes(child.path))
} else if len(child.path) == 4*common.HashLength {
paths = append(paths, hexToKeybytes(child.path[:2*common.HashLength]))
paths = append(paths, hexToKeybytes(child.path[2*common.HashLength:]))
}
if err := req.callback(paths, child.path, node, req.hash); err != nil {
return nil, err return nil, err
} }
} }
......
...@@ -37,9 +37,20 @@ var ( ...@@ -37,9 +37,20 @@ var (
) )
// LeafCallback is a callback type invoked when a trie operation reaches a leaf // LeafCallback is a callback type invoked when a trie operation reaches a leaf
// node. It's used by state sync and commit to allow handling external references // node.
// between account and storage tries. //
type LeafCallback func(path []byte, leaf []byte, parent common.Hash) error // The paths is a path tuple identifying a particular trie node either in a single
// trie (account) or a layered trie (account -> storage). Each path in the tuple
// is in the raw format(32 bytes).
//
// The hexpath is a composite hexary path identifying the trie node. All the key
// bytes are converted to the hexary nibbles and composited with the parent path
// if the trie node is in a layered trie.
//
// It's used by state sync and commit to allow handling external references
// between account and storage tries. And also it's used in the state healing
// for extracting the raw states(leaf nodes) with corresponding paths.
type LeafCallback func(paths [][]byte, hexpath []byte, leaf []byte, parent common.Hash) error
// Trie is a Merkle Patricia Trie. // Trie is a Merkle Patricia Trie.
// The zero value is an empty trie with no database. // The zero value is an empty trie with no database.
......
...@@ -569,7 +569,7 @@ func BenchmarkCommitAfterHash(b *testing.B) { ...@@ -569,7 +569,7 @@ func BenchmarkCommitAfterHash(b *testing.B) {
benchmarkCommitAfterHash(b, nil) benchmarkCommitAfterHash(b, nil)
}) })
var a account var a account
onleaf := func(path []byte, leaf []byte, parent common.Hash) error { onleaf := func(paths [][]byte, hexpath []byte, leaf []byte, parent common.Hash) error {
rlp.DecodeBytes(leaf, &a) rlp.DecodeBytes(leaf, &a)
return nil return nil
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment