triedb/pathdb: configure different node hasher in pathdb (#31008)

As the node hash scheme in verkle and merkle are totally different, the
original default node hasher in pathdb is no longer suitable. Therefore,
this pull request configures different node hasher respectively.
pull/30982/head
rjl493456442 1 month ago committed by GitHub
parent 033de2a05b
commit 82e963e5c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      core/genesis.go
  2. 5
      core/state/stateupdate.go
  3. 11
      core/types/hashes.go
  4. 3
      internal/ethapi/override/override_test.go
  5. 4
      trie/trie_reader.go
  6. 62
      triedb/pathdb/database.go
  7. 4
      triedb/pathdb/database_test.go
  8. 15
      triedb/pathdb/journal.go
  9. 5
      triedb/pathdb/layertree.go

@ -127,8 +127,12 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
} }
// Create an ephemeral in-memory database for computing hash, // Create an ephemeral in-memory database for computing hash,
// all the derived states will be discarded to not pollute disk. // all the derived states will be discarded to not pollute disk.
emptyRoot := types.EmptyRootHash
if isVerkle {
emptyRoot = types.EmptyVerkleHash
}
db := rawdb.NewMemoryDatabase() db := rawdb.NewMemoryDatabase()
statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb.NewDatabase(db, config), nil)) statedb, err := state.New(emptyRoot, state.NewDatabase(triedb.NewDatabase(db, config), nil))
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
@ -148,7 +152,11 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
// flushAlloc is very similar with hash, but the main difference is all the // flushAlloc is very similar with hash, but the main difference is all the
// generated states will be persisted into the given database. // generated states will be persisted into the given database.
func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, error) { func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, error) {
statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb, nil)) emptyRoot := types.EmptyRootHash
if triedb.IsVerkle() {
emptyRoot = types.EmptyVerkleHash
}
statedb, err := state.New(emptyRoot, state.NewDatabase(triedb, nil))
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }

@ -20,7 +20,6 @@ import (
"maps" "maps"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
) )
@ -133,8 +132,8 @@ func newStateUpdate(originRoot common.Hash, root common.Hash, deletes map[common
} }
} }
return &stateUpdate{ return &stateUpdate{
originRoot: types.TrieRootHash(originRoot), originRoot: originRoot,
root: types.TrieRootHash(root), root: root,
accounts: accounts, accounts: accounts,
accountsOrigin: accountsOrigin, accountsOrigin: accountsOrigin,
storages: storages, storages: storages,

@ -19,7 +19,6 @@ package types
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
) )
var ( var (
@ -47,13 +46,3 @@ var (
// EmptyVerkleHash is the known hash of an empty verkle trie. // EmptyVerkleHash is the known hash of an empty verkle trie.
EmptyVerkleHash = common.Hash{} EmptyVerkleHash = common.Hash{}
) )
// TrieRootHash returns the hash itself if it's non-empty or the predefined
// emptyHash one instead.
func TrieRootHash(hash common.Hash) common.Hash {
if hash == (common.Hash{}) {
log.Error("Zero trie root hash!")
return EmptyRootHash
}
return hash
}

@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"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/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
) )
@ -36,7 +37,7 @@ func (p *precompileContract) Run(input []byte) ([]byte, error) { return nil, nil
func TestStateOverrideMovePrecompile(t *testing.T) { func TestStateOverrideMovePrecompile(t *testing.T) {
db := state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) db := state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
statedb, err := state.New(common.Hash{}, db) statedb, err := state.New(types.EmptyRootHash, db)
if err != nil { if err != nil {
t.Fatalf("failed to create statedb: %v", err) t.Fatalf("failed to create statedb: %v", err)
} }

@ -19,7 +19,6 @@ package trie
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/triedb/database" "github.com/ethereum/go-ethereum/triedb/database"
) )
@ -34,9 +33,6 @@ type trieReader struct {
// newTrieReader initializes the trie reader with the given node reader. // newTrieReader initializes the trie reader with the given node reader.
func newTrieReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*trieReader, error) { func newTrieReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*trieReader, error) {
if stateRoot == (common.Hash{}) || stateRoot == types.EmptyRootHash { if stateRoot == (common.Hash{}) || stateRoot == types.EmptyRootHash {
if stateRoot == (common.Hash{}) {
log.Error("Zero state root hash!")
}
return &trieReader{owner: owner}, nil return &trieReader{owner: owner}, nil
} }
reader, err := db.NodeReader(stateRoot) reader, err := db.NodeReader(stateRoot)

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-verkle"
) )
const ( const (
@ -148,6 +149,29 @@ var Defaults = &Config{
// ReadOnly is the config in order to open database in read only mode. // ReadOnly is the config in order to open database in read only mode.
var ReadOnly = &Config{ReadOnly: true} var ReadOnly = &Config{ReadOnly: true}
// nodeHasher is the function to compute the hash of supplied node blob.
type nodeHasher func([]byte) (common.Hash, error)
// merkleNodeHasher computes the hash of the given merkle node.
func merkleNodeHasher(blob []byte) (common.Hash, error) {
if len(blob) == 0 {
return types.EmptyRootHash, nil
}
return crypto.Keccak256Hash(blob), nil
}
// verkleNodeHasher computes the hash of the given verkle node.
func verkleNodeHasher(blob []byte) (common.Hash, error) {
if len(blob) == 0 {
return types.EmptyVerkleHash, nil
}
n, err := verkle.ParseNode(blob, 0)
if err != nil {
return common.Hash{}, err
}
return n.Commit().Bytes(), nil
}
// Database is a multiple-layered structure for maintaining in-memory states // Database is a multiple-layered structure for maintaining in-memory states
// along with its dirty trie nodes. It consists of one persistent base layer // along with its dirty trie nodes. It consists of one persistent base layer
// backed by a key-value store, on top of which arbitrarily many in-memory diff // backed by a key-value store, on top of which arbitrarily many in-memory diff
@ -164,9 +188,10 @@ type Database struct {
// readOnly is the flag whether the mutation is allowed to be applied. // readOnly is the flag whether the mutation is allowed to be applied.
// It will be set automatically when the database is journaled during // It will be set automatically when the database is journaled during
// the shutdown to reject all following unexpected mutations. // the shutdown to reject all following unexpected mutations.
readOnly bool // Flag if database is opened in read only mode readOnly bool // Flag if database is opened in read only mode
waitSync bool // Flag if database is deactivated due to initial state sync waitSync bool // Flag if database is deactivated due to initial state sync
isVerkle bool // Flag if database is used for verkle tree isVerkle bool // Flag if database is used for verkle tree
hasher nodeHasher // Trie node hasher
config *Config // Configuration for database config *Config // Configuration for database
diskdb ethdb.Database // Persistent storage for matured trie nodes diskdb ethdb.Database // Persistent storage for matured trie nodes
@ -184,19 +209,21 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
} }
config = config.sanitize() config = config.sanitize()
db := &Database{
readOnly: config.ReadOnly,
isVerkle: isVerkle,
config: config,
diskdb: diskdb,
hasher: merkleNodeHasher,
}
// Establish a dedicated database namespace tailored for verkle-specific // Establish a dedicated database namespace tailored for verkle-specific
// data, ensuring the isolation of both verkle and merkle tree data. It's // data, ensuring the isolation of both verkle and merkle tree data. It's
// important to note that the introduction of a prefix won't lead to // important to note that the introduction of a prefix won't lead to
// substantial storage overhead, as the underlying database will efficiently // substantial storage overhead, as the underlying database will efficiently
// compress the shared key prefix. // compress the shared key prefix.
if isVerkle { if isVerkle {
diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix)) db.diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix))
} db.hasher = verkleNodeHasher
db := &Database{
readOnly: config.ReadOnly,
isVerkle: isVerkle,
config: config,
diskdb: diskdb,
} }
// Construct the layer tree by resolving the in-disk singleton state // Construct the layer tree by resolving the in-disk singleton state
// and in-memory layer journal. // and in-memory layer journal.
@ -277,6 +304,8 @@ func (db *Database) repairHistory() error {
// //
// The passed in maps(nodes, states) will be retained to avoid copying everything. // The passed in maps(nodes, states) will be retained to avoid copying everything.
// Therefore, these maps must not be changed afterwards. // Therefore, these maps must not be changed afterwards.
//
// The supplied parentRoot and root must be a valid trie hash value.
func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *StateSetWithOrigin) error { func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *StateSetWithOrigin) error {
// Hold the lock to prevent concurrent mutations. // Hold the lock to prevent concurrent mutations.
db.lock.Lock() db.lock.Lock()
@ -350,10 +379,9 @@ func (db *Database) Enable(root common.Hash) error {
return errDatabaseReadOnly return errDatabaseReadOnly
} }
// Ensure the provided state root matches the stored one. // Ensure the provided state root matches the stored one.
root = types.TrieRootHash(root) stored, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil))
stored := types.EmptyRootHash if err != nil {
if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { return err
stored = crypto.Keccak256Hash(blob)
} }
if stored != root { if stored != root {
return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root) return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root)
@ -389,6 +417,8 @@ func (db *Database) Enable(root common.Hash) error {
// Recover rollbacks the database to a specified historical point. // Recover rollbacks the database to a specified historical point.
// The state is supported as the rollback destination only if it's // The state is supported as the rollback destination only if it's
// canonical state and the corresponding trie histories are existent. // canonical state and the corresponding trie histories are existent.
//
// The supplied root must be a valid trie hash value.
func (db *Database) Recover(root common.Hash) error { func (db *Database) Recover(root common.Hash) error {
db.lock.Lock() db.lock.Lock()
defer db.lock.Unlock() defer db.lock.Unlock()
@ -401,7 +431,6 @@ func (db *Database) Recover(root common.Hash) error {
return errors.New("state rollback is non-supported") return errors.New("state rollback is non-supported")
} }
// Short circuit if the target state is not recoverable // Short circuit if the target state is not recoverable
root = types.TrieRootHash(root)
if !db.Recoverable(root) { if !db.Recoverable(root) {
return errStateUnrecoverable return errStateUnrecoverable
} }
@ -434,9 +463,10 @@ func (db *Database) Recover(root common.Hash) error {
} }
// Recoverable returns the indicator if the specified state is recoverable. // Recoverable returns the indicator if the specified state is recoverable.
//
// The supplied root must be a valid trie hash value.
func (db *Database) Recoverable(root common.Hash) bool { func (db *Database) Recoverable(root common.Hash) bool {
// Ensure the requested state is a known state. // Ensure the requested state is a known state.
root = types.TrieRootHash(root)
id := rawdb.ReadStateID(db.diskdb, root) id := rawdb.ReadStateID(db.diskdb, root)
if id == nil { if id == nil {
return false return false

@ -458,8 +458,8 @@ func TestDatabaseRecoverable(t *testing.T) {
// Initial state should be recoverable // Initial state should be recoverable
{types.EmptyRootHash, true}, {types.EmptyRootHash, true},
// Initial state should be recoverable // common.Hash{} is not a valid state root for revert
{common.Hash{}, true}, {common.Hash{}, false},
// Layers below current disk layer are recoverable // Layers below current disk layer are recoverable
{tester.roots[index-1], true}, {tester.roots[index-1], true},

@ -26,7 +26,6 @@ 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/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -93,9 +92,9 @@ func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
// loadLayers loads a pre-existing state layer backed by a key-value store. // loadLayers loads a pre-existing state layer backed by a key-value store.
func (db *Database) loadLayers() layer { func (db *Database) loadLayers() layer {
// Retrieve the root node of persistent state. // Retrieve the root node of persistent state.
var root = types.EmptyRootHash root, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil))
if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { if err != nil {
root = crypto.Keccak256Hash(blob) log.Crit("Failed to compute node hash", "err", err)
} }
// Load the layers by resolving the journal // Load the layers by resolving the journal
head, err := db.loadJournal(root) head, err := db.loadJournal(root)
@ -236,6 +235,8 @@ func (dl *diffLayer) journal(w io.Writer) error {
// This is meant to be used during shutdown to persist the layer without // This is meant to be used during shutdown to persist the layer without
// flattening everything down (bad for reorgs). And this function will mark the // flattening everything down (bad for reorgs). And this function will mark the
// database as read-only to prevent all following mutation to disk. // database as read-only to prevent all following mutation to disk.
//
// The supplied root must be a valid trie hash value.
func (db *Database) Journal(root common.Hash) error { func (db *Database) Journal(root common.Hash) error {
// Retrieve the head layer to journal from. // Retrieve the head layer to journal from.
l := db.tree.get(root) l := db.tree.get(root)
@ -265,9 +266,9 @@ func (db *Database) Journal(root common.Hash) error {
} }
// Secondly write out the state root in disk, ensure all layers // Secondly write out the state root in disk, ensure all layers
// on top are continuous with disk. // on top are continuous with disk.
diskRoot := types.EmptyRootHash diskRoot, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil))
if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { if err != nil {
diskRoot = crypto.Keccak256Hash(blob) return err
} }
if err := rlp.Encode(journal, diskRoot); err != nil { if err := rlp.Encode(journal, diskRoot); err != nil {
return err return err

@ -22,7 +22,6 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
) )
@ -62,7 +61,7 @@ func (tree *layerTree) get(root common.Hash) layer {
tree.lock.RLock() tree.lock.RLock()
defer tree.lock.RUnlock() defer tree.lock.RUnlock()
return tree.layers[types.TrieRootHash(root)] return tree.layers[root]
} }
// forEach iterates the stored layers inside and applies the // forEach iterates the stored layers inside and applies the
@ -92,7 +91,6 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
// //
// Although we could silently ignore this internally, it should be the caller's // Although we could silently ignore this internally, it should be the caller's
// responsibility to avoid even attempting to insert such a layer. // responsibility to avoid even attempting to insert such a layer.
root, parentRoot = types.TrieRootHash(root), types.TrieRootHash(parentRoot)
if root == parentRoot { if root == parentRoot {
return errors.New("layer cycle") return errors.New("layer cycle")
} }
@ -112,7 +110,6 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
// are crossed. All diffs beyond the permitted number are flattened downwards. // are crossed. All diffs beyond the permitted number are flattened downwards.
func (tree *layerTree) cap(root common.Hash, layers int) error { func (tree *layerTree) cap(root common.Hash, layers int) error {
// Retrieve the head layer to cap from // Retrieve the head layer to cap from
root = types.TrieRootHash(root)
l := tree.get(root) l := tree.get(root)
if l == nil { if l == nil {
return fmt.Errorf("triedb layer [%#x] missing", root) return fmt.Errorf("triedb layer [%#x] missing", root)

Loading…
Cancel
Save