From b122acbc0dbdf412504f5590efdfbbfc7966783f Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 16 Aug 2023 14:58:34 +0800 Subject: [PATCH] core, trie/triedb/pathdb: adjust pathdb for verkle --- cmd/geth/dbcmd.go | 11 ++- core/rawdb/accessors_trie.go | 49 +++++------- core/rawdb/ancient_scheme.go | 17 ++-- core/rawdb/ancient_utils.go | 13 ++- core/rawdb/database.go | 20 +++++ core/rawdb/schema.go | 9 +++ trie/database.go | 37 +++++++-- trie/sync.go | 8 +- trie/trie_reader.go | 13 --- trie/triedb/hashdb/database.go | 5 -- trie/triedb/pathdb/database.go | 114 ++++++++++++++++++++++----- trie/triedb/pathdb/database_test.go | 14 ++-- trie/triedb/pathdb/difflayer.go | 51 ++++-------- trie/triedb/pathdb/difflayer_test.go | 33 ++++---- trie/triedb/pathdb/disklayer.go | 80 +++++-------------- trie/triedb/pathdb/errors.go | 16 ---- trie/triedb/pathdb/hasher.go | 51 ++++++++++++ trie/triedb/pathdb/history_test.go | 2 +- trie/triedb/pathdb/journal.go | 40 +++++----- trie/triedb/pathdb/layertree.go | 2 +- trie/triedb/pathdb/metrics.go | 1 + trie/triedb/pathdb/nodebuffer.go | 71 ++++++++--------- trie/trienode/node.go | 21 ++--- trie/triestate/state.go | 4 +- 24 files changed, 382 insertions(+), 300 deletions(-) create mode 100644 trie/triedb/pathdb/hasher.go diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 1d885bd58d..ee02110f03 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -220,11 +220,18 @@ func removeDB(ctx *cli.Context) error { ancientDir = config.Node.ResolvePath(ancientDir) } // Delete state data - statePaths := []string{rootDir, filepath.Join(ancientDir, rawdb.StateFreezerName)} + statePaths := []string{ + rootDir, + filepath.Join(ancientDir, rawdb.MerkleStateFreezerName), + filepath.Join(ancientDir, rawdb.VerkleStateFreezerName), + } confirmAndRemoveDB(statePaths, "state data", ctx, removeStateDataFlag.Name) // Delete ancient chain - chainPaths := []string{filepath.Join(ancientDir, rawdb.ChainFreezerName)} + chainPaths := []string{filepath.Join( + ancientDir, + rawdb.ChainFreezerName, + )} confirmAndRemoveDB(chainPaths, "ancient chain", ctx, removeChainDataFlag.Name) return nil } diff --git a/core/rawdb/accessors_trie.go b/core/rawdb/accessors_trie.go index ea3367db36..5dcfcb84e6 100644 --- a/core/rawdb/accessors_trie.go +++ b/core/rawdb/accessors_trie.go @@ -65,16 +65,10 @@ func (h *hasher) release() { hasherPool.Put(h) } -// ReadAccountTrieNode retrieves the account trie node and the associated node -// hash with the specified node path. -func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) ([]byte, common.Hash) { - data, err := db.Get(accountTrieNodeKey(path)) - if err != nil { - return nil, common.Hash{} - } - h := newHasher() - defer h.release() - return data, h.hash(data) +// ReadAccountTrieNode retrieves the account trie node with the specified node path. +func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) []byte { + data, _ := db.Get(accountTrieNodeKey(path)) + return data } // HasAccountTrieNode checks the account trie node presence with the specified @@ -113,16 +107,10 @@ func DeleteAccountTrieNode(db ethdb.KeyValueWriter, path []byte) { } } -// ReadStorageTrieNode retrieves the storage trie node and the associated node -// hash with the specified node path. -func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) ([]byte, common.Hash) { - data, err := db.Get(storageTrieNodeKey(accountHash, path)) - if err != nil { - return nil, common.Hash{} - } - h := newHasher() - defer h.release() - return data, h.hash(data) +// ReadStorageTrieNode retrieves the storage trie node with the specified node path. +func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) []byte { + data, _ := db.Get(storageTrieNodeKey(accountHash, path)) + return data } // HasStorageTrieNode checks the storage trie node presence with the provided @@ -220,16 +208,15 @@ func ReadTrieNode(db ethdb.KeyValueReader, owner common.Hash, path []byte, hash case HashScheme: return ReadLegacyTrieNode(db, hash) case PathScheme: - var ( - blob []byte - nHash common.Hash - ) + var blob []byte if owner == (common.Hash{}) { - blob, nHash = ReadAccountTrieNode(db, path) + blob = ReadAccountTrieNode(db, path) } else { - blob, nHash = ReadStorageTrieNode(db, owner, path) + blob = ReadStorageTrieNode(db, owner, path) } - if nHash != hash { + h := newHasher() + defer h.release() + if h.hash(blob) != hash { return nil } return blob @@ -287,8 +274,12 @@ func DeleteTrieNode(db ethdb.KeyValueWriter, owner common.Hash, path []byte, has // ReadStateScheme reads the state scheme of persistent state, or none // if the state is not present in database. func ReadStateScheme(db ethdb.Reader) string { - // Check if state in path-based scheme is present - blob, _ := ReadAccountTrieNode(db, nil) + // Check if state in path-based scheme is present, it can be either + // merkle tree or verkle tree. + blob := ReadAccountTrieNode(db, nil) + if len(blob) == 0 { + blob, _ = db.Get(verkleTrieNodeKey(nil)) // FIX HACK(rjl493456442) + } if len(blob) != 0 { return PathScheme } diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index e88867af0e..7dd6bd0652 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -68,14 +68,21 @@ var stateFreezerNoSnappy = map[string]bool{ // The list of identifiers of ancient stores. var ( - ChainFreezerName = "chain" // the folder name of chain segment ancient store. - StateFreezerName = "state" // the folder name of reverse diff ancient store. + ChainFreezerName = "chain" // the folder name of chain segment ancient store. + MerkleStateFreezerName = "state" // the folder name of reverse diff ancient store. + VerkleStateFreezerName = "state_verkle" // the folder name of reverse diff ancient store. ) // freezers the collections of all builtin freezers. -var freezers = []string{ChainFreezerName, StateFreezerName} +var freezers = []string{ChainFreezerName, MerkleStateFreezerName, VerkleStateFreezerName} // NewStateFreezer initializes the freezer for state history. -func NewStateFreezer(ancientDir string, readOnly bool) (*ResettableFreezer, error) { - return NewResettableFreezer(filepath.Join(ancientDir, StateFreezerName), "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerNoSnappy) +func NewStateFreezer(ancientDir string, verkle bool, readOnly bool) (*ResettableFreezer, error) { + var name string + if verkle { + name = filepath.Join(ancientDir, VerkleStateFreezerName) + } else { + name = filepath.Join(ancientDir, MerkleStateFreezerName) + } + return NewResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerNoSnappy) } diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 428cda544b..6804d7a91a 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -88,21 +88,18 @@ func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) { } infos = append(infos, info) - case StateFreezerName: - if ReadStateScheme(db) != PathScheme { - continue - } + case MerkleStateFreezerName, VerkleStateFreezerName: datadir, err := db.AncientDatadir() if err != nil { return nil, err } - f, err := NewStateFreezer(datadir, true) + f, err := NewStateFreezer(datadir, freezer == VerkleStateFreezerName, true) if err != nil { - return nil, err + continue // might be possible the state freezer is not existent } defer f.Close() - info, err := inspect(StateFreezerName, stateFreezerNoSnappy, f) + info, err := inspect(freezer, stateFreezerNoSnappy, f) if err != nil { return nil, err } @@ -127,7 +124,7 @@ func InspectFreezerTable(ancient string, freezerName string, tableName string, s switch freezerName { case ChainFreezerName: path, tables = resolveChainFreezerDir(ancient), chainFreezerNoSnappy - case StateFreezerName: + case MerkleStateFreezerName, VerkleStateFreezerName: path, tables = filepath.Join(ancient, freezerName), stateFreezerNoSnappy default: return fmt.Errorf("unknown freezer, supported ones: %v", freezers) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 27a9ec7412..fd64288c99 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -480,6 +480,10 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { beaconHeaders stat cliqueSnaps stat + // Verkle statistics + verkleTries stat + verkleStateLookups stat + // Les statistic chtTrieNodes stat bloomTrieNodes stat @@ -549,6 +553,20 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { bytes.HasPrefix(key, BloomTrieIndexPrefix) || bytes.HasPrefix(key, BloomTriePrefix): // Bloomtrie sub bloomTrieNodes.Add(size) + case bytes.HasPrefix(key, VerklePrefix): + remain := key[len(VerklePrefix):] + switch { + case IsAccountTrieNode(remain): + verkleTries.Add(size) + case bytes.HasPrefix(remain, stateIDPrefix) && len(remain) == len(stateIDPrefix)+common.HashLength: + verkleStateLookups.Add(size) + case bytes.Equal(remain, persistentStateIDKey): + metadata.Add(size) + case bytes.Equal(remain, trieJournalKey): + metadata.Add(size) + case bytes.Equal(remain, snapshotSyncStatusKey): + metadata.Add(size) + } default: var accounted bool for _, meta := range [][]byte{ @@ -595,6 +613,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()}, {"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()}, {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, + {"Key-Value store", "Verkle trie nodes", verkleTries.Size(), verkleTries.Count()}, + {"Key-Value store", "Verkle trie state lookups", verkleStateLookups.Size(), verkleStateLookups.Count()}, {"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()}, {"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()}, } diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 11cf5b40fe..c4da8e0b3b 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -117,6 +117,10 @@ var ( trieNodeStoragePrefix = []byte("O") // trieNodeStoragePrefix + accountHash + hexPath -> trie node stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id + // VerklePrefix is the prefix of verkle states(verkle trie nodes, + // trie journal, persistent state id, state id lookups). + VerklePrefix = []byte("v") + PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage configPrefix = []byte("ethereum-config-") // config prefix for the db genesisPrefix = []byte("ethereum-genesis-") // genesis state prefix for the db @@ -270,6 +274,11 @@ func accountTrieNodeKey(path []byte) []byte { return append(trieNodeAccountPrefix, path...) } +// verkleTrieNodeKey = verklePrefix + trieNodeAccountPrefix + nodePath. +func verkleTrieNodeKey(path []byte) []byte { + return append(VerklePrefix, append(trieNodeAccountPrefix, path...)...) +} + // storageTrieNodeKey = trieNodeStoragePrefix + accountHash + nodePath. func storageTrieNodeKey(accountHash common.Hash, path []byte) []byte { buf := make([]byte, len(trieNodeStoragePrefix)+common.HashLength+len(path)) diff --git a/trie/database.go b/trie/database.go index e20f7ef903..5c422a2af1 100644 --- a/trie/database.go +++ b/trie/database.go @@ -20,6 +20,7 @@ import ( "errors" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie/triedb/hashdb" @@ -28,6 +29,23 @@ import ( "github.com/ethereum/go-ethereum/trie/triestate" ) +// Reader wraps the Node method of a backing trie store. +type Reader interface { + // Node retrieves the trie node blob with the provided trie identifier, node path and + // the corresponding node hash. No error will be returned if the node is not found. + // + // When looking up nodes in the account trie, 'owner' is the zero hash. For contract + // storage trie nodes, 'owner' is the hash of the account address that containing the + // storage. + // + // Notably, the provided hash might be useless in path mode if hash check is + // configured as disabled, e.g. in the verkle context. + // + // Don't modify the returned byte slice since it's not deep-copied and still + // be referenced by database. + Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) +} + // Config defines all necessary options for database. type Config struct { Preimages bool // Flag whether the preimage of node key is recorded @@ -40,15 +58,21 @@ type Config struct { // default settings. var HashDefaults = &Config{ Preimages: false, + IsVerkle: false, HashDB: hashdb.Defaults, } +// PathDefaults represents a config for using path-based scheme with +// default settings. +var PathDefaults = &Config{ + Preimages: false, + IsVerkle: false, + PathDB: pathdb.Defaults, +} + // backend defines the methods needed to access/update trie nodes in different // state scheme. type backend interface { - // Scheme returns the identifier of used storage scheme. - Scheme() string - // Initialized returns an indicator if the state data is already initialized // according to the state scheme. Initialized(genesisRoot common.Hash) bool @@ -106,7 +130,7 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { log.Crit("Both 'hash' and 'path' mode are configured") } if config.PathDB != nil { - db.backend = pathdb.New(diskdb, config.PathDB) + db.backend = pathdb.New(diskdb, config.PathDB, config.IsVerkle) } else { db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) } @@ -172,7 +196,10 @@ func (db *Database) Initialized(genesisRoot common.Hash) bool { // Scheme returns the node scheme used in the database. func (db *Database) Scheme() string { - return db.backend.Scheme() + if db.config.PathDB != nil { + return rawdb.PathScheme + } + return rawdb.HashScheme } // Close flushes the dangling preimages to disk and closes the trie database. diff --git a/trie/sync.go b/trie/sync.go index 589d28364b..86a45f535b 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -19,6 +19,7 @@ package trie import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/crypto" "sync" "github.com/ethereum/go-ethereum/common" @@ -691,13 +692,12 @@ func (s *Sync) hasNode(owner common.Hash, path []byte, hash common.Hash) (exists } // If node is running with path scheme, check the presence with node path. var blob []byte - var dbHash common.Hash if owner == (common.Hash{}) { - blob, dbHash = rawdb.ReadAccountTrieNode(s.database, path) + blob = rawdb.ReadAccountTrieNode(s.database, path) } else { - blob, dbHash = rawdb.ReadStorageTrieNode(s.database, owner, path) + blob = rawdb.ReadStorageTrieNode(s.database, owner, path) } - exists = hash == dbHash + exists = hash == crypto.Keccak256Hash(blob) inconsistent = !exists && len(blob) != 0 return exists, inconsistent } diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 4215964559..a2f7bd56a2 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -23,19 +23,6 @@ import ( "github.com/ethereum/go-ethereum/trie/triestate" ) -// Reader wraps the Node method of a backing trie store. -type Reader interface { - // Node retrieves the trie node blob with the provided trie identifier, node path and - // the corresponding node hash. No error will be returned if the node is not found. - // - // When looking up nodes in the account trie, 'owner' is the zero hash. For contract - // storage trie nodes, 'owner' is the hash of the account address that containing the - // storage. - // - // TODO(rjl493456442): remove the 'hash' parameter, it's redundant in PBSS. - Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) -} - // trieReader is a wrapper of the underlying node reader. It's not safe // for concurrent usage. type trieReader struct { diff --git a/trie/triedb/hashdb/database.go b/trie/triedb/hashdb/database.go index e45ccdba32..b38455baf5 100644 --- a/trie/triedb/hashdb/database.go +++ b/trie/triedb/hashdb/database.go @@ -624,11 +624,6 @@ func (db *Database) Close() error { return nil } -// Scheme returns the node scheme used in the database. -func (db *Database) Scheme() string { - return rawdb.HashScheme -} - // Reader retrieves a node reader belonging to the given state root. // An error will be returned if the requested state is not available. func (db *Database) Reader(root common.Hash) (*reader, error) { diff --git a/trie/triedb/pathdb/database.go b/trie/triedb/pathdb/database.go index f2d6cea635..5130ddfa88 100644 --- a/trie/triedb/pathdb/database.go +++ b/trie/triedb/pathdb/database.go @@ -54,14 +54,34 @@ const ( DefaultBufferSize = 64 * 1024 * 1024 ) +const ( + locDirtyCache = "dirty" + locCleanCache = "clean" + locDisk = "disk" + locDiffLayer = "diff" +) + +// nodeLoc is a helpful structure that contains the location where the node +// is found, as it's useful for debugging purposes. +type nodeLoc struct { + loc string + depth int +} + +// string returns the string representation of node location. +func (loc *nodeLoc) string() string { + return fmt.Sprintf("loc: %s, depth: %d", loc.loc, loc.depth) +} + // layer is the interface implemented by all state layers which includes some // public methods and some additional methods for internal usage. type layer interface { // Node retrieves the trie node with the node info. An error will be returned - // if the read operation exits abnormally. For example, if the layer is already - // stale, or the associated state is regarded as corrupted. Notably, no error - // will be returned if the requested node is not found in database. - Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) + // if the read operation exits abnormally. Specifically, if the layer is + // already stale. + // + // Note, no error will be returned if the requested node is not found in database. + node(owner common.Hash, path []byte, depth int) ([]byte, *nodeLoc, error) // rootHash returns the root hash for which this layer was made. rootHash() common.Hash @@ -76,7 +96,7 @@ type layer interface { // the provided dirty trie nodes along with the state change set. // // Note, the maps are retained by the method to avoid copying everything. - update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string]*trienode.Node, states *triestate.Set) *diffLayer + update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string][]byte, states *triestate.Set) *diffLayer // journal commits an entire diff hierarchy to disk into a single journal entry. // This is meant to be used during shutdown to persist the layer without @@ -86,10 +106,11 @@ type layer interface { // Config contains the settings for database. type Config struct { - StateHistory uint64 // Number of recent blocks to maintain state history for - CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes - DirtyCacheSize int // Maximum memory allowance (in bytes) for caching dirty nodes - ReadOnly bool // Flag whether the database is opened in read only mode. + StateHistory uint64 // Number of recent blocks to maintain state history for + CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes + DirtyCacheSize int // Maximum memory allowance (in bytes) for caching dirty nodes + ReadOnly bool // Flag whether the database is opened in read only mode. + Hasher func([]byte) common.Hash // Function to compute the hash of node } // sanitize checks the provided user configurations and changes anything that's @@ -100,6 +121,9 @@ func (c *Config) sanitize() *Config { log.Warn("Sanitizing invalid node buffer size", "provided", common.StorageSize(conf.DirtyCacheSize), "updated", common.StorageSize(maxBufferSize)) conf.DirtyCacheSize = maxBufferSize } + if conf.Hasher == nil { + conf.Hasher = hashNode + } return &conf } @@ -113,6 +137,12 @@ var Defaults = &Config{ // ReadOnly is the config in order to open database in read only mode. var ReadOnly = &Config{ReadOnly: true} +// readOption contains the configurations for reader. +type readOption struct { + checkHash bool + hasher func([]byte) common.Hash +} + // Database is a multiple-layered structure for maintaining in-memory trie nodes. // It consists of one persistent base layer backed by a key-value store, on top // of which arbitrarily many in-memory diff layers are stacked. The memory diffs @@ -135,23 +165,33 @@ type Database struct { diskdb ethdb.Database // Persistent storage for matured trie nodes tree *layerTree // The group for all known layers freezer *rawdb.ResettableFreezer // Freezer for storing trie histories, nil possible in tests + readOption *readOption // Options for constructing reader. lock sync.RWMutex // Lock to prevent mutations from happening at the same time } // New attempts to load an already existing layer from a persistent key-value // store (with a number of memory layers from a journal). If the journal is not // matched with the base persistent layer, all the recorded diff layers are discarded. -func New(diskdb ethdb.Database, config *Config) *Database { +func New(diskdb ethdb.Database, config *Config, verkle bool) *Database { if config == nil { config = Defaults } config = config.sanitize() + // Establish a dedicated database namespace tailored for verkle-specific + // data, ensuring the isolation of both verkle and mpt tree data. It's + // important to note that the introduction of a prefix won't lead to + // substantial storage overhead, as the underlying database will efficiently + // compress the shared key prefix. + if verkle { + diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix)) + } db := &Database{ readOnly: config.ReadOnly, bufferSize: config.DirtyCacheSize, config: config, diskdb: diskdb, + readOption: &readOption{checkHash: !verkle, hasher: config.Hasher}, } // Construct the layer tree by resolving the in-disk singleton state // and in-memory layer journal. @@ -164,7 +204,7 @@ func New(diskdb ethdb.Database, config *Config) *Database { // mechanism also ensures that at most one **non-readOnly** database // is opened at the same time to prevent accidental mutation. if ancient, err := diskdb.AncientDatadir(); err == nil && ancient != "" && !db.readOnly { - freezer, err := rawdb.NewStateFreezer(ancient, false) + freezer, err := rawdb.NewStateFreezer(ancient, verkle, false) if err != nil { log.Crit("Failed to open state history freezer", "err", err) } @@ -207,13 +247,50 @@ func New(diskdb ethdb.Database, config *Config) *Database { return db } +// reader implements the Reader interface, providing the functionalities to +// retrieve trie nodes by wrapping the internal state layer. +type reader struct { + layer layer + option *readOption +} + +// Node implements trie.Reader interface, retrieving the node with specified +// node info. Don't modify the returned byte slice since it's not deep-copied +// and still be referenced by database. +func (r *reader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) { + blob, loc, err := r.layer.node(owner, path, 0) + if err != nil { + return nil, err + } + // Skip the hash comparison if it's disabled. Normally it will be configured + // in the verkle context because slim format is used in verkle which doesn't + // store the node hash at all to reduce read/write amplification. + if !r.option.checkHash { + return blob, nil + } + if got := r.option.hasher(blob); got != hash { + switch loc.loc { + case locCleanCache: + cleanFalseMeter.Mark(1) + case locDirtyCache: + dirtyFalseMeter.Mark(1) + case locDiffLayer: + diffFalseMeter.Mark(1) + case locDisk: + diskFalseMeter.Mark(1) + } + return nil, fmt.Errorf("unexpected node: (%x %v), %x!=%x, %s", owner, path, hash, got, loc.string()) + } + return blob, nil +} + // Reader retrieves a layer belonging to the given state root. -func (db *Database) Reader(root common.Hash) (layer, error) { - l := db.tree.get(root) - if l == nil { +func (db *Database) Reader(root common.Hash) (*reader, error) { + layer := db.tree.get(root) + if layer == nil { return nil, fmt.Errorf("state %#x is not available", root) } - return l, nil + return &reader{layer: layer, option: db.readOption}, nil } // Update adds a new layer into the tree, if that can be linked to an existing @@ -297,7 +374,7 @@ func (db *Database) Enable(root common.Hash) error { } // Ensure the provided state root matches the stored one. root = types.TrieRootHash(root) - _, stored := rawdb.ReadAccountTrieNode(db.diskdb, nil) + stored := db.config.Hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) if stored != root { return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root) } @@ -467,11 +544,6 @@ func (db *Database) SetBufferSize(size int) error { return db.tree.bottom().setBufferSize(db.bufferSize) } -// Scheme returns the node scheme used in the database. -func (db *Database) Scheme() string { - return rawdb.PathScheme -} - // modifyAllowed returns the indicator if mutation is allowed. This function // assumes the db.lock is already held. func (db *Database) modifyAllowed() error { diff --git a/trie/triedb/pathdb/database_test.go b/trie/triedb/pathdb/database_test.go index e7bd469993..49f31f5a58 100644 --- a/trie/triedb/pathdb/database_test.go +++ b/trie/triedb/pathdb/database_test.go @@ -103,7 +103,7 @@ func newTester(t *testing.T, historyLimit uint64) *tester { StateHistory: historyLimit, CleanCacheSize: 256 * 1024, DirtyCacheSize: 256 * 1024, - }) + }, false) obj = &tester{ db: db, preimages: make(map[common.Hash]common.Address), @@ -447,7 +447,7 @@ func TestDisable(t *testing.T) { tester := newTester(t, 0) defer tester.release() - _, stored := rawdb.ReadAccountTrieNode(tester.db.diskdb, nil) + stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) if err := tester.db.Disable(); err != nil { t.Fatal("Failed to deactivate database") } @@ -511,7 +511,7 @@ func TestJournal(t *testing.T) { t.Errorf("Failed to journal, err: %v", err) } tester.db.Close() - tester.db = New(tester.db.diskdb, nil) + tester.db = New(tester.db.diskdb, nil, false) // Verify states including disk layer and all diff on top. for i := 0; i < len(tester.roots); i++ { @@ -535,7 +535,9 @@ func TestCorruptedJournal(t *testing.T) { t.Errorf("Failed to journal, err: %v", err) } tester.db.Close() - _, root := rawdb.ReadAccountTrieNode(tester.db.diskdb, nil) + + rootBlob := rawdb.ReadAccountTrieNode(tester.db.diskdb, nil) + root := crypto.Keccak256Hash(rootBlob) // Mutate the journal in disk, it should be regarded as invalid blob := rawdb.ReadTrieJournal(tester.db.diskdb) @@ -543,7 +545,7 @@ func TestCorruptedJournal(t *testing.T) { rawdb.WriteTrieJournal(tester.db.diskdb, blob) // Verify states, all not-yet-written states should be discarded - tester.db = New(tester.db.diskdb, nil) + tester.db = New(tester.db.diskdb, nil, false) for i := 0; i < len(tester.roots); i++ { if tester.roots[i] == root { if err := tester.verifyState(root); err != nil { @@ -574,7 +576,7 @@ func TestTailTruncateHistory(t *testing.T) { defer tester.release() tester.db.Close() - tester.db = New(tester.db.diskdb, &Config{StateHistory: 10}) + tester.db = New(tester.db.diskdb, &Config{StateHistory: 10}, false) head, err := tester.db.freezer.Ancients() if err != nil { diff --git a/trie/triedb/pathdb/difflayer.go b/trie/triedb/pathdb/difflayer.go index 10567715d2..9e82b9cb2a 100644 --- a/trie/triedb/pathdb/difflayer.go +++ b/trie/triedb/pathdb/difflayer.go @@ -22,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" ) @@ -33,19 +32,19 @@ import ( // made to the state, that have not yet graduated into a semi-immutable state. type diffLayer struct { // Immutables - root common.Hash // Root hash to which this layer diff belongs to - id uint64 // Corresponding state id - block uint64 // Associated block number - nodes map[common.Hash]map[string]*trienode.Node // Cached trie nodes indexed by owner and path - states *triestate.Set // Associated state change set for building history - memory uint64 // Approximate guess as to how much memory we use + root common.Hash // Root hash to which this layer diff belongs to + id uint64 // Corresponding state id + block uint64 // Associated block number + nodes map[common.Hash]map[string][]byte // Cached trie nodes indexed by owner and path + states *triestate.Set // Associated state change set for building history + memory uint64 // Approximate guess as to how much memory we use parent layer // Parent layer modified by this one, never nil, **can be changed** lock sync.RWMutex // Lock used to protect parent } // newDiffLayer creates a new diff layer on top of an existing layer. -func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string]*trienode.Node, states *triestate.Set) *diffLayer { +func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string][]byte, states *triestate.Set) *diffLayer { var ( size int64 count int @@ -60,11 +59,11 @@ func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes } for _, subset := range nodes { for path, n := range subset { - dl.memory += uint64(n.Size() + len(path)) - size += int64(len(n.Blob) + len(path)) + size += int64(len(n) + len(path)) } count += len(subset) } + dl.memory = uint64(size) if states != nil { dl.memory += uint64(states.Size()) } @@ -95,10 +94,9 @@ func (dl *diffLayer) parentLayer() layer { return dl.parent } -// node retrieves the node with provided node information. It's the internal -// version of Node function with additional accessed layer tracked. No error -// will be returned if node is not found. -func (dl *diffLayer) node(owner common.Hash, path []byte, hash common.Hash, depth int) ([]byte, error) { +// node implements the layer interface, retrieving the trie node blob with the +// provided node information. No error will be returned if the node is not found. +func (dl *diffLayer) node(owner common.Hash, path []byte, depth int) ([]byte, *nodeLoc, error) { // Hold the lock, ensure the parent won't be changed during the // state accessing. dl.lock.RLock() @@ -109,36 +107,19 @@ func (dl *diffLayer) node(owner common.Hash, path []byte, hash common.Hash, dept if ok { n, ok := subset[string(path)] if ok { - // If the trie node is not hash matched, or marked as removed, - // bubble up an error here. It shouldn't happen at all. - if n.Hash != hash { - dirtyFalseMeter.Mark(1) - log.Error("Unexpected trie node in diff layer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) - return nil, newUnexpectedNodeError("diff", hash, n.Hash, owner, path, n.Blob) - } dirtyHitMeter.Mark(1) dirtyNodeHitDepthHist.Update(int64(depth)) - dirtyReadMeter.Mark(int64(len(n.Blob))) - return n.Blob, nil + dirtyReadMeter.Mark(int64(len(n))) + return n, &nodeLoc{loc: locDiffLayer, depth: depth}, nil } } // Trie node unknown to this layer, resolve from parent - if diff, ok := dl.parent.(*diffLayer); ok { - return diff.node(owner, path, hash, depth+1) - } - // Failed to resolve through diff layers, fallback to disk layer - return dl.parent.Node(owner, path, hash) -} - -// Node implements the layer interface, retrieving the trie node blob with the -// provided node information. No error will be returned if the node is not found. -func (dl *diffLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) { - return dl.node(owner, path, hash, 0) + return dl.parent.node(owner, path, depth+1) } // update implements the layer interface, creating a new layer on top of the // existing layer tree with the specified data items. -func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string]*trienode.Node, states *triestate.Set) *diffLayer { +func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string][]byte, states *triestate.Set) *diffLayer { return newDiffLayer(dl, root, id, block, nodes, states) } diff --git a/trie/triedb/pathdb/difflayer_test.go b/trie/triedb/pathdb/difflayer_test.go index 9b5907c3c5..6af527b911 100644 --- a/trie/triedb/pathdb/difflayer_test.go +++ b/trie/triedb/pathdb/difflayer_test.go @@ -23,12 +23,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/trie/testutil" - "github.com/ethereum/go-ethereum/trie/trienode" ) func emptyLayer() *diskLayer { return &diskLayer{ - db: New(rawdb.NewMemoryDatabase(), nil), + db: New(rawdb.NewMemoryDatabase(), nil, false), buffer: newNodeBuffer(DefaultBufferSize, nil, 0), } } @@ -57,23 +56,21 @@ func BenchmarkSearch1Layer(b *testing.B) { benchmarkSearch(b, 127, 128) } func benchmarkSearch(b *testing.B, depth int, total int) { var ( npath []byte - nhash common.Hash nblob []byte ) // First, we set up 128 diff layers, with 3K items each fill := func(parent layer, index int) *diffLayer { - nodes := make(map[common.Hash]map[string]*trienode.Node) - nodes[common.Hash{}] = make(map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string][]byte) + nodes[common.Hash{}] = make(map[string][]byte) for i := 0; i < 3000; i++ { var ( path = testutil.RandBytes(32) - node = testutil.RandomNode() + blob = testutil.RandBytes(100) ) - nodes[common.Hash{}][string(path)] = trienode.New(node.Hash, node.Blob) + nodes[common.Hash{}][string(path)] = blob if npath == nil && depth == index { npath = common.CopyBytes(path) - nblob = common.CopyBytes(node.Blob) - nhash = node.Hash + nblob = common.CopyBytes(blob) } } return newDiffLayer(parent, common.Hash{}, 0, 0, nodes, nil) @@ -90,7 +87,7 @@ func benchmarkSearch(b *testing.B, depth int, total int) { err error ) for i := 0; i < b.N; i++ { - have, err = layer.Node(common.Hash{}, npath, nhash) + have, _, err = layer.node(common.Hash{}, npath, 0) if err != nil { b.Fatal(err) } @@ -108,14 +105,14 @@ func benchmarkSearch(b *testing.B, depth int, total int) { func BenchmarkPersist(b *testing.B) { // First, we set up 128 diff layers, with 3K items each fill := func(parent layer) *diffLayer { - nodes := make(map[common.Hash]map[string]*trienode.Node) - nodes[common.Hash{}] = make(map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string][]byte) + nodes[common.Hash{}] = make(map[string][]byte) for i := 0; i < 3000; i++ { var ( path = testutil.RandBytes(32) - node = testutil.RandomNode() + blob = testutil.RandBytes(100) ) - nodes[common.Hash{}][string(path)] = trienode.New(node.Hash, node.Blob) + nodes[common.Hash{}][string(path)] = blob } return newDiffLayer(parent, common.Hash{}, 0, 0, nodes, nil) } @@ -145,14 +142,14 @@ func BenchmarkJournal(b *testing.B) { // First, we set up 128 diff layers, with 3K items each fill := func(parent layer) *diffLayer { - nodes := make(map[common.Hash]map[string]*trienode.Node) - nodes[common.Hash{}] = make(map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string][]byte) + nodes[common.Hash{}] = make(map[string][]byte) for i := 0; i < 3000; i++ { var ( path = testutil.RandBytes(32) - node = testutil.RandomNode() + blob = testutil.RandBytes(100) ) - nodes[common.Hash{}][string(path)] = trienode.New(node.Hash, node.Blob) + nodes[common.Hash{}][string(path)] = blob } // TODO(rjl493456442) a non-nil state set is expected. return newDiffLayer(parent, common.Hash{}, 0, 0, nodes, nil) diff --git a/trie/triedb/pathdb/disklayer.go b/trie/triedb/pathdb/disklayer.go index ef697cbce8..2e34ca56c9 100644 --- a/trie/triedb/pathdb/disklayer.go +++ b/trie/triedb/pathdb/disklayer.go @@ -24,11 +24,8 @@ import ( "github.com/VictoriaMetrics/fastcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" - "golang.org/x/crypto/sha3" ) // diskLayer is a low level persistent layer built on top of a key-value store. @@ -95,27 +92,25 @@ func (dl *diskLayer) markStale() { dl.stale = true } -// Node implements the layer interface, retrieving the trie node with the +// node implements the layer interface, retrieving the trie node with the // provided node info. No error will be returned if the node is not found. -func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) { +func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, *nodeLoc, error) { dl.lock.RLock() defer dl.lock.RUnlock() if dl.stale { - return nil, errSnapshotStale + return nil, nil, errSnapshotStale } // Try to retrieve the trie node from the not-yet-written // node buffer first. Note the buffer is lock free since // it's impossible to mutate the buffer before tagging the // layer as stale. - n, err := dl.buffer.node(owner, path, hash) - if err != nil { - return nil, err - } - if n != nil { + n, found := dl.buffer.node(owner, path) + if found { dirtyHitMeter.Mark(1) - dirtyReadMeter.Mark(int64(len(n.Blob))) - return n.Blob, nil + dirtyReadMeter.Mark(int64(len(n))) + dirtyNodeHitDepthHist.Update(int64(depth)) + return n, &nodeLoc{loc: locDirtyCache, depth: depth}, nil } dirtyMissMeter.Mark(1) @@ -123,45 +118,31 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b key := cacheKey(owner, path) if dl.cleans != nil { if blob := dl.cleans.Get(nil, key); len(blob) > 0 { - h := newHasher() - defer h.release() - - got := h.hash(blob) - if got == hash { - cleanHitMeter.Mark(1) - cleanReadMeter.Mark(int64(len(blob))) - return blob, nil - } - cleanFalseMeter.Mark(1) - log.Error("Unexpected trie node in clean cache", "owner", owner, "path", path, "expect", hash, "got", got) + cleanHitMeter.Mark(1) + cleanReadMeter.Mark(int64(len(blob))) + dirtyNodeHitDepthHist.Update(int64(depth)) + return blob, &nodeLoc{loc: locCleanCache, depth: depth}, nil } cleanMissMeter.Mark(1) } // Try to retrieve the trie node from the disk. - var ( - nBlob []byte - nHash common.Hash - ) + var blob []byte if owner == (common.Hash{}) { - nBlob, nHash = rawdb.ReadAccountTrieNode(dl.db.diskdb, path) + blob = rawdb.ReadAccountTrieNode(dl.db.diskdb, path) } else { - nBlob, nHash = rawdb.ReadStorageTrieNode(dl.db.diskdb, owner, path) + blob = rawdb.ReadStorageTrieNode(dl.db.diskdb, owner, path) } - if nHash != hash { - diskFalseMeter.Mark(1) - log.Error("Unexpected trie node in disk", "owner", owner, "path", path, "expect", hash, "got", nHash) - return nil, newUnexpectedNodeError("disk", hash, nHash, owner, path, nBlob) + if dl.cleans != nil && len(blob) > 0 { + dl.cleans.Set(key, blob) + cleanWriteMeter.Mark(int64(len(blob))) + dirtyNodeHitDepthHist.Update(int64(depth)) } - if dl.cleans != nil && len(nBlob) > 0 { - dl.cleans.Set(key, nBlob) - cleanWriteMeter.Mark(int64(len(nBlob))) - } - return nBlob, nil + return blob, &nodeLoc{loc: locDisk, depth: depth}, nil } // update implements the layer interface, returning a new diff layer on top // with the given state set. -func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string]*trienode.Node, states *triestate.Set) *diffLayer { +func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes map[common.Hash]map[string][]byte, states *triestate.Set) *diffLayer { return newDiffLayer(dl, root, id, block, nodes, states) } @@ -317,22 +298,3 @@ func (dl *diskLayer) resetCache() { dl.cleans.Reset() } } - -// hasher is used to compute the sha256 hash of the provided data. -type hasher struct{ sha crypto.KeccakState } - -var hasherPool = sync.Pool{ - New: func() interface{} { return &hasher{sha: sha3.NewLegacyKeccak256().(crypto.KeccakState)} }, -} - -func newHasher() *hasher { - return hasherPool.Get().(*hasher) -} - -func (h *hasher) hash(data []byte) common.Hash { - return crypto.HashData(h.sha, data) -} - -func (h *hasher) release() { - hasherPool.Put(h) -} diff --git a/trie/triedb/pathdb/errors.go b/trie/triedb/pathdb/errors.go index 78ee4459fe..86c4df6fce 100644 --- a/trie/triedb/pathdb/errors.go +++ b/trie/triedb/pathdb/errors.go @@ -18,10 +18,6 @@ package pathdb import ( "errors" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" ) var ( @@ -45,16 +41,4 @@ var ( // errStateUnrecoverable is returned if state is required to be reverted to // a destination without associated state history available. errStateUnrecoverable = errors.New("state is unrecoverable") - - // errUnexpectedNode is returned if the requested node with specified path is - // not hash matched with expectation. - errUnexpectedNode = errors.New("unexpected node") ) - -func newUnexpectedNodeError(loc string, expHash common.Hash, gotHash common.Hash, owner common.Hash, path []byte, blob []byte) error { - blobHex := "nil" - if len(blob) > 0 { - blobHex = hexutil.Encode(blob) - } - return fmt.Errorf("%w, loc: %s, node: (%x %v), %x!=%x, blob: %s", errUnexpectedNode, loc, owner, path, expHash, gotHash, blobHex) -} diff --git a/trie/triedb/pathdb/hasher.go b/trie/triedb/pathdb/hasher.go new file mode 100644 index 0000000000..506bb5467c --- /dev/null +++ b/trie/triedb/pathdb/hasher.go @@ -0,0 +1,51 @@ +// Copyright 2023 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 ( + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/sha3" +) + +// hasher is used to compute the sha256 hash of the provided data. +type hasher struct{ sha crypto.KeccakState } + +var hasherPool = sync.Pool{ + New: func() interface{} { return &hasher{sha: sha3.NewLegacyKeccak256().(crypto.KeccakState)} }, +} + +func newHasher() *hasher { + return hasherPool.Get().(*hasher) +} + +func (h *hasher) hash(data []byte) common.Hash { + return crypto.HashData(h.sha, data) +} + +func (h *hasher) release() { + hasherPool.Put(h) +} + +func hashNode(node []byte) common.Hash { + h := newHasher() + defer h.release() + + return h.hash(node) +} diff --git a/trie/triedb/pathdb/history_test.go b/trie/triedb/pathdb/history_test.go index a3257441de..01e2552f73 100644 --- a/trie/triedb/pathdb/history_test.go +++ b/trie/triedb/pathdb/history_test.go @@ -270,7 +270,7 @@ func TestTruncateOutOfRange(t *testing.T) { // openFreezer initializes the freezer instance for storing state histories. func openFreezer(datadir string, readOnly bool) (*rawdb.ResettableFreezer, error) { - return rawdb.NewStateFreezer(datadir, readOnly) + return rawdb.NewStateFreezer(datadir, false, readOnly) } func compareSet[k comparable](a, b map[k][]byte) bool { diff --git a/trie/triedb/pathdb/journal.go b/trie/triedb/pathdb/journal.go index ac770763e3..c8f5d02115 100644 --- a/trie/triedb/pathdb/journal.go +++ b/trie/triedb/pathdb/journal.go @@ -26,10 +26,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" ) @@ -115,9 +113,10 @@ func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { // loadLayers loads a pre-existing state layer backed by a key-value store. func (db *Database) loadLayers() layer { // Retrieve the root node of persistent state. - _, root := rawdb.ReadAccountTrieNode(db.diskdb, nil) - root = types.TrieRootHash(root) - + var root = types.EmptyRootHash + if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { + root = db.config.Hasher(blob) + } // Load the layers by resolving the journal head, err := db.loadJournal(root) if err == nil { @@ -157,14 +156,14 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { if err := r.Decode(&encoded); err != nil { return nil, fmt.Errorf("load disk nodes: %v", err) } - nodes := make(map[common.Hash]map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string][]byte) for _, entry := range encoded { - subset := make(map[string]*trienode.Node) + subset := make(map[string][]byte) for _, n := range entry.Nodes { if len(n.Blob) > 0 { - subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(n.Blob), n.Blob) + subset[string(n.Path)] = n.Blob } else { - subset[string(n.Path)] = trienode.NewDeleted() + subset[string(n.Path)] = nil } } nodes[entry.Owner] = subset @@ -195,14 +194,14 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { if err := r.Decode(&encoded); err != nil { return nil, fmt.Errorf("load diff nodes: %v", err) } - nodes := make(map[common.Hash]map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string][]byte) for _, entry := range encoded { - subset := make(map[string]*trienode.Node) + subset := make(map[string][]byte) for _, n := range entry.Nodes { if len(n.Blob) > 0 { - subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(n.Blob), n.Blob) + subset[string(n.Path)] = n.Blob } else { - subset[string(n.Path)] = trienode.NewDeleted() + subset[string(n.Path)] = nil } } nodes[entry.Owner] = subset @@ -264,7 +263,7 @@ func (dl *diskLayer) journal(w io.Writer) error { for owner, subset := range dl.buffer.nodes { entry := journalNodes{Owner: owner} for path, node := range subset { - entry.Nodes = append(entry.Nodes, journalNode{Path: []byte(path), Blob: node.Blob}) + entry.Nodes = append(entry.Nodes, journalNode{Path: []byte(path), Blob: node}) } nodes = append(nodes, entry) } @@ -297,7 +296,7 @@ func (dl *diffLayer) journal(w io.Writer) error { for owner, subset := range dl.nodes { entry := journalNodes{Owner: owner} for path, node := range subset { - entry.Nodes = append(entry.Nodes, journalNode{Path: []byte(path), Blob: node.Blob}) + entry.Nodes = append(entry.Nodes, journalNode{Path: []byte(path), Blob: node}) } nodes = append(nodes, entry) } @@ -363,14 +362,13 @@ func (db *Database) Journal(root common.Hash) error { if err := rlp.Encode(journal, journalVersion); err != nil { return err } - // The stored state in disk might be empty, convert the - // root to emptyRoot in this case. - _, diskroot := rawdb.ReadAccountTrieNode(db.diskdb, nil) - diskroot = types.TrieRootHash(diskroot) - // Secondly write out the state root in disk, ensure all layers // on top are continuous with disk. - if err := rlp.Encode(journal, diskroot); err != nil { + diskRoot := types.EmptyRootHash + if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { + diskRoot = db.config.Hasher(blob) + } + if err := rlp.Encode(journal, diskRoot); err != nil { return err } // Finally write out the journal of each layer in reverse order. diff --git a/trie/triedb/pathdb/layertree.go b/trie/triedb/pathdb/layertree.go index d314779910..d3eb978093 100644 --- a/trie/triedb/pathdb/layertree.go +++ b/trie/triedb/pathdb/layertree.go @@ -101,7 +101,7 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6 if parent == nil { return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot) } - l := parent.update(root, parent.stateID()+1, block, nodes.Flatten(), states) + l := parent.update(root, parent.stateID()+1, block, nodes.Slim(), states) tree.lock.Lock() tree.layers[l.rootHash()] = l diff --git a/trie/triedb/pathdb/metrics.go b/trie/triedb/pathdb/metrics.go index 9e2b1dcbf5..a250f703cb 100644 --- a/trie/triedb/pathdb/metrics.go +++ b/trie/triedb/pathdb/metrics.go @@ -33,6 +33,7 @@ var ( cleanFalseMeter = metrics.NewRegisteredMeter("pathdb/clean/false", nil) dirtyFalseMeter = metrics.NewRegisteredMeter("pathdb/dirty/false", nil) diskFalseMeter = metrics.NewRegisteredMeter("pathdb/disk/false", nil) + diffFalseMeter = metrics.NewRegisteredMeter("pathdb/diff/false", nil) commitTimeTimer = metrics.NewRegisteredTimer("pathdb/commit/time", nil) commitNodesMeter = metrics.NewRegisteredMeter("pathdb/commit/nodes", nil) diff --git a/trie/triedb/pathdb/nodebuffer.go b/trie/triedb/pathdb/nodebuffer.go index 4a7d328b9a..ed8b554081 100644 --- a/trie/triedb/pathdb/nodebuffer.go +++ b/trie/triedb/pathdb/nodebuffer.go @@ -17,37 +17,36 @@ package pathdb import ( + "bytes" "fmt" "time" "github.com/VictoriaMetrics/fastcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/trie/trienode" ) // nodebuffer is a collection of modified trie nodes to aggregate the disk // write. The content of the nodebuffer must be checked before diving into // disk (since it basically is not-yet-written data). type nodebuffer struct { - layers uint64 // The number of diff layers aggregated inside - size uint64 // The size of aggregated writes - limit uint64 // The maximum memory allowance in bytes - nodes map[common.Hash]map[string]*trienode.Node // The dirty node set, mapped by owner and path + layers uint64 // The number of diff layers aggregated inside + size uint64 // The size of aggregated writes + limit uint64 // The maximum memory allowance in bytes + nodes map[common.Hash]map[string][]byte // The dirty node set, mapped by owner and path } // newNodeBuffer initializes the node buffer with the provided nodes. -func newNodeBuffer(limit int, nodes map[common.Hash]map[string]*trienode.Node, layers uint64) *nodebuffer { +func newNodeBuffer(limit int, nodes map[common.Hash]map[string][]byte, layers uint64) *nodebuffer { if nodes == nil { - nodes = make(map[common.Hash]map[string]*trienode.Node) + nodes = make(map[common.Hash]map[string][]byte) } var size uint64 for _, subset := range nodes { for path, n := range subset { - size += uint64(len(n.Blob) + len(path)) + size += uint64(len(n) + len(path)) } } return &nodebuffer{ @@ -59,28 +58,22 @@ func newNodeBuffer(limit int, nodes map[common.Hash]map[string]*trienode.Node, l } // node retrieves the trie node with given node info. -func (b *nodebuffer) node(owner common.Hash, path []byte, hash common.Hash) (*trienode.Node, error) { +func (b *nodebuffer) node(owner common.Hash, path []byte) ([]byte, bool) { subset, ok := b.nodes[owner] if !ok { - return nil, nil + return nil, false } n, ok := subset[string(path)] if !ok { - return nil, nil + return nil, false } - if n.Hash != hash { - dirtyFalseMeter.Mark(1) - log.Error("Unexpected trie node in node buffer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) - return nil, newUnexpectedNodeError("dirty", hash, n.Hash, owner, path, n.Blob) - } - return n, nil + return n, true } // commit merges the dirty nodes into the nodebuffer. This operation won't take // the ownership of the nodes map which belongs to the bottom-most diff layer. -// It will just hold the node references from the given map which are safe to -// copy. -func (b *nodebuffer) commit(nodes map[common.Hash]map[string]*trienode.Node) *nodebuffer { +// It will hold the node references from the given map which are safe to copy. +func (b *nodebuffer) commit(nodes map[common.Hash]map[string][]byte) *nodebuffer { var ( delta int64 overwrite int64 @@ -94,21 +87,21 @@ func (b *nodebuffer) commit(nodes map[common.Hash]map[string]*trienode.Node) *no // The nodes belong to original diff layer are still accessible even // after merging, thus the ownership of nodes map should still belong // to original layer and any mutation on it should be prevented. - current = make(map[string]*trienode.Node) + current = make(map[string][]byte) for path, n := range subset { current[path] = n - delta += int64(len(n.Blob) + len(path)) + delta += int64(len(n) + len(path)) } b.nodes[owner] = current continue } for path, n := range subset { if orig, exist := current[path]; !exist { - delta += int64(len(n.Blob) + len(path)) + delta += int64(len(n) + len(path)) } else { - delta += int64(len(n.Blob) - len(orig.Blob)) + delta += int64(len(n) - len(orig)) overwrite++ - overwriteSize += int64(len(orig.Blob) + len(path)) + overwriteSize += int64(len(orig) + len(path)) } current[path] = n } @@ -124,7 +117,7 @@ func (b *nodebuffer) commit(nodes map[common.Hash]map[string]*trienode.Node) *no // revert is the reverse operation of commit. It also merges the provided nodes // into the nodebuffer, the difference is that the provided node set should // revert the changes made by the last state transition. -func (b *nodebuffer) revert(db ethdb.KeyValueReader, nodes map[common.Hash]map[string]*trienode.Node) error { +func (b *nodebuffer) revert(db ethdb.KeyValueReader, nodes map[common.Hash]map[string][]byte) error { // Short circuit if no embedded state transition to revert. if b.layers == 0 { return errStateUnrecoverable @@ -153,20 +146,20 @@ func (b *nodebuffer) revert(db ethdb.KeyValueReader, nodes map[common.Hash]map[s // // In case of database rollback, don't panic if this "clean" // node occurs which is not present in buffer. - var nhash common.Hash + var blob []byte if owner == (common.Hash{}) { - _, nhash = rawdb.ReadAccountTrieNode(db, []byte(path)) + blob = rawdb.ReadAccountTrieNode(db, []byte(path)) } else { - _, nhash = rawdb.ReadStorageTrieNode(db, owner, []byte(path)) + blob = rawdb.ReadStorageTrieNode(db, owner, []byte(path)) } // Ignore the clean node in the case described above. - if nhash == n.Hash { + if bytes.Equal(blob, n) { continue } - panic(fmt.Sprintf("non-existent node (%x %v) blob: %v", owner, path, crypto.Keccak256Hash(n.Blob).Hex())) + panic(fmt.Sprintf("non-existent node (%x %v) blob: %v", owner, path, n)) } current[path] = n - delta += int64(len(n.Blob)) - int64(len(orig.Blob)) + delta += int64(len(n)) - int64(len(orig)) } } b.updateSize(delta) @@ -189,7 +182,7 @@ func (b *nodebuffer) updateSize(delta int64) { func (b *nodebuffer) reset() { b.layers = 0 b.size = 0 - b.nodes = make(map[common.Hash]map[string]*trienode.Node) + b.nodes = make(map[common.Hash]map[string][]byte) } // empty returns an indicator if nodebuffer contains any state transition inside. @@ -238,10 +231,10 @@ func (b *nodebuffer) flush(db ethdb.KeyValueStore, clean *fastcache.Cache, id ui // writeNodes writes the trie nodes into the provided database batch. // Note this function will also inject all the newly written nodes // into clean cache. -func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.Node, clean *fastcache.Cache) (total int) { +func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string][]byte, clean *fastcache.Cache) (total int) { for owner, subset := range nodes { for path, n := range subset { - if n.IsDeleted() { + if len(n) == 0 { if owner == (common.Hash{}) { rawdb.DeleteAccountTrieNode(batch, []byte(path)) } else { @@ -252,12 +245,12 @@ func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.No } } else { if owner == (common.Hash{}) { - rawdb.WriteAccountTrieNode(batch, []byte(path), n.Blob) + rawdb.WriteAccountTrieNode(batch, []byte(path), n) } else { - rawdb.WriteStorageTrieNode(batch, owner, []byte(path), n.Blob) + rawdb.WriteStorageTrieNode(batch, owner, []byte(path), n) } if clean != nil { - clean.Set(cacheKey(owner, []byte(path)), n.Blob) + clean.Set(cacheKey(owner, []byte(path)), n) } } } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 95315c2e9a..f9215273e0 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -28,15 +28,10 @@ import ( // node hash. It is general enough that can be used to represent trie node // corresponding to different trie implementations. type Node struct { - Hash common.Hash // Node hash, empty for deleted node + Hash common.Hash // Node hash, empty for deleted node or verkle node Blob []byte // Encoded node blob, nil for the deleted node } -// Size returns the total memory size used by this node. -func (n *Node) Size() int { - return len(n.Blob) + common.HashLength -} - // IsDeleted returns the indicator if the node is marked as deleted. func (n *Node) IsDeleted() bool { return len(n.Blob) == 0 @@ -189,11 +184,17 @@ func (set *MergedNodeSet) Merge(other *NodeSet) error { return nil } -// Flatten returns a two-dimensional map for internal nodes. -func (set *MergedNodeSet) Flatten() map[common.Hash]map[string]*Node { - nodes := make(map[common.Hash]map[string]*Node) +// Slim creates a node map by assembling nodes from the given set while excluding +// any other fields present in the MergedNodeSet. The node blobs are referenced +// directly without deep-copied due to the fact that they are immutable. +func (set *MergedNodeSet) Slim() map[common.Hash]map[string][]byte { + nodes := make(map[common.Hash]map[string][]byte) for owner, set := range set.Sets { - nodes[owner] = set.Nodes + subset := make(map[string][]byte) + for path, node := range set.Nodes { + subset[path] = node.Blob + } + nodes[owner] = subset } return nodes } diff --git a/trie/triestate/state.go b/trie/triestate/state.go index 4c47e9c397..1124efb737 100644 --- a/trie/triestate/state.go +++ b/trie/triestate/state.go @@ -105,7 +105,7 @@ type context struct { // Apply traverses the provided state diffs, apply them in the associated // post-state and return the generated dirty trie nodes. The state can be // loaded via the provided trie loader. -func Apply(prevRoot common.Hash, postRoot common.Hash, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, loader TrieLoader) (map[common.Hash]map[string]*trienode.Node, error) { +func Apply(prevRoot common.Hash, postRoot common.Hash, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, loader TrieLoader) (map[common.Hash]map[string][]byte, error) { tr, err := loader.OpenTrie(postRoot) if err != nil { return nil, err @@ -139,7 +139,7 @@ func Apply(prevRoot common.Hash, postRoot common.Hash, accounts map[common.Addre if err := ctx.nodes.Merge(result); err != nil { return nil, err } - return ctx.nodes.Flatten(), nil + return ctx.nodes.Slim(), nil } // updateAccount the account was present in prev-state, and may or may not