diff --git a/core/state/statedb.go b/core/state/statedb.go index 3b706002e76..d673e428346 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -19,6 +19,7 @@ package state import ( "fmt" + "reflect" "sort" "time" @@ -47,6 +48,19 @@ type revision struct { journalIndex int } +type snapshotTree interface { + Snapshot(root common.Hash) snapshot.Snapshot + Update( + blockRoot common.Hash, + parentRoot common.Hash, + destructs map[common.Hash]struct{}, + accounts map[common.Hash][]byte, + storage map[common.Hash]map[common.Hash][]byte, + ) error + StorageIterator(root common.Hash, account common.Hash, seek common.Hash) (snapshot.StorageIterator, error) + Cap(root common.Hash, layers int) error +} + // StateDB structs within the ethereum protocol are used to store anything // within the merkle trie. StateDBs take care of caching and storing // nested states. It's the general query interface to retrieve: @@ -63,7 +77,7 @@ type StateDB struct { prefetcher *triePrefetcher trie Trie hasher crypto.KeccakState - snaps *snapshot.Tree // Nil if snapshot is not available + snaps snapshotTree // Nil if snapshot is not available snap snapshot.Snapshot // Nil if snapshot is not available // originalRoot is the pre-state root, before any changes were made. @@ -141,7 +155,15 @@ type StateDB struct { } // New creates a new state from a given trie. -func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { +func New(root common.Hash, db Database, snaps snapshotTree) (*StateDB, error) { + if snaps != nil { + // XXX: Make sure we treat incoming `nil` ptrs as `nil` values, not an + // interface to a nil ptr + v := reflect.ValueOf(snaps) + if v.Kind() == reflect.Ptr && v.IsNil() { + snaps = nil + } + } tr, err := db.OpenTrie(root) if err != nil { return nil, err diff --git a/params/config.go b/params/config.go index 1708bc78de7..4690569f612 100644 --- a/params/config.go +++ b/params/config.go @@ -880,7 +880,7 @@ func newTimestampCompatError(what string, storedtime, newtime *uint64) *ConfigCo NewTime: newtime, RewindToTime: 0, } - if rew != nil { + if rew != nil && *rew != 0 { err.RewindToTime = *rew - 1 } return err @@ -890,7 +890,15 @@ func (err *ConfigCompatError) Error() string { if err.StoredBlock != nil { return fmt.Sprintf("mismatching %s in database (have block %d, want block %d, rewindto block %d)", err.What, err.StoredBlock, err.NewBlock, err.RewindToBlock) } - return fmt.Sprintf("mismatching %s in database (have timestamp %d, want timestamp %d, rewindto timestamp %d)", err.What, err.StoredTime, err.NewTime, err.RewindToTime) + + if err.StoredTime == nil && err.NewTime == nil { + return "" + } else if err.StoredTime == nil && err.NewTime != nil { + return fmt.Sprintf("mismatching %s in database (have timestamp nil, want timestamp %d, rewindto timestamp %d)", err.What, *err.NewTime, err.RewindToTime) + } else if err.StoredTime != nil && err.NewTime == nil { + return fmt.Sprintf("mismatching %s in database (have timestamp %d, want timestamp nil, rewindto timestamp %d)", err.What, *err.StoredTime, err.RewindToTime) + } + return fmt.Sprintf("mismatching %s in database (have timestamp %d, want timestamp %d, rewindto timestamp %d)", err.What, *err.StoredTime, *err.NewTime, err.RewindToTime) } // Rules wraps ChainConfig and is merely syntactic sugar or can be used for functions diff --git a/params/config_test.go b/params/config_test.go index 707566aa46f..6b222d3a673 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/ava-labs/libevm/common/math" + "github.com/stretchr/testify/require" ) func TestCheckCompatible(t *testing.T) { @@ -137,3 +138,20 @@ func TestConfigRules(t *testing.T) { t.Errorf("expected %v to be shanghai", stamp) } } + +func TestTimestampCompatError(t *testing.T) { + require.Equal(t, new(ConfigCompatError).Error(), "") + + errWhat := "Shanghai fork timestamp" + require.Equal(t, newTimestampCompatError(errWhat, nil, newUint64(1681338455)).Error(), + "mismatching Shanghai fork timestamp in database (have timestamp nil, want timestamp 1681338455, rewindto timestamp 1681338454)") + + require.Equal(t, newTimestampCompatError(errWhat, newUint64(1681338455), nil).Error(), + "mismatching Shanghai fork timestamp in database (have timestamp 1681338455, want timestamp nil, rewindto timestamp 1681338454)") + + require.Equal(t, newTimestampCompatError(errWhat, newUint64(1681338455), newUint64(600624000)).Error(), + "mismatching Shanghai fork timestamp in database (have timestamp 1681338455, want timestamp 600624000, rewindto timestamp 600623999)") + + require.Equal(t, newTimestampCompatError(errWhat, newUint64(0), newUint64(1681338455)).Error(), + "mismatching Shanghai fork timestamp in database (have timestamp 0, want timestamp 1681338455, rewindto timestamp 0)") +} diff --git a/triedb/database.go b/triedb/database.go index 4ce4de54a5d..447d901e8de 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -27,15 +27,14 @@ import ( "github.com/ava-labs/libevm/trie/triestate" "github.com/ava-labs/libevm/triedb/database" "github.com/ava-labs/libevm/triedb/hashdb" - "github.com/ava-labs/libevm/triedb/pathdb" ) // Config defines all necessary options for database. type Config struct { - Preimages bool // Flag whether the preimage of node key is recorded - IsVerkle bool // Flag whether the db is holding a verkle tree - HashDB *hashdb.Config // Configs for hash-based scheme - PathDB *pathdb.Config // Configs for experimental path-based scheme + Preimages bool // Flag whether the preimage of node key is recorded + IsVerkle bool // Flag whether the db is holding a verkle tree + HashDB hashBackender // Configs for hash-based scheme + PathDB pathBackender // Configs for experimental path-based scheme } // HashDefaults represents a config for using hash-based scheme with @@ -45,6 +44,15 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } +type Backend backend + +type hashBackender interface { + New(diskdb ethdb.Database, resolver hashdb.ChildResolver) database.HashBackend +} +type pathBackender interface { + New(diskdb ethdb.Database) database.PathBackend +} + // backend defines the methods needed to access/update trie nodes in different // state scheme. type backend interface { @@ -76,6 +84,10 @@ type backend interface { // Close closes the trie database backend and releases all held resources. Close() error + + // Reader returns a node reader associated with the specific state. + // An error will be returned if the specified state is not available. + Reader(stateRoot common.Hash) (database.Reader, error) } // Database is the wrapper of the underlying backend which is shared by different @@ -108,7 +120,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 = config.PathDB.New(diskdb) } else { var resolver hashdb.ChildResolver if config.IsVerkle { @@ -117,7 +129,11 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { } else { resolver = trie.MerkleResolver{} } - db.backend = hashdb.New(diskdb, config.HashDB, resolver) + if config.HashDB == nil { + // some tests don't set this yet pass a non-nil config + config.HashDB = hashdb.Defaults + } + db.backend = config.HashDB.New(diskdb, resolver) } return db } @@ -125,13 +141,7 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { // Reader returns a reader for accessing all trie nodes with provided state root. // An error will be returned if the requested state is not available. func (db *Database) Reader(blockRoot common.Hash) (database.Reader, error) { - switch b := db.backend.(type) { - case *hashdb.Database: - return b.Reader(blockRoot) - case *pathdb.Database: - return b.Reader(blockRoot) - } - return nil, errors.New("unknown backend") + return db.backend.Reader(blockRoot) } // Update performs a state transition by committing dirty nodes contained in the @@ -221,7 +231,7 @@ func (db *Database) InsertPreimage(preimages map[common.Hash][]byte) { // // It's only supported by hash-based database and will return an error for others. func (db *Database) Cap(limit common.StorageSize) error { - hdb, ok := db.backend.(*hashdb.Database) + hdb, ok := db.backend.(database.HashBackend) if !ok { return errors.New("not supported") } @@ -237,7 +247,7 @@ func (db *Database) Cap(limit common.StorageSize) error { // // It's only supported by hash-based database and will return an error for others. func (db *Database) Reference(root common.Hash, parent common.Hash) error { - hdb, ok := db.backend.(*hashdb.Database) + hdb, ok := db.backend.(database.HashBackend) if !ok { return errors.New("not supported") } @@ -248,7 +258,7 @@ func (db *Database) Reference(root common.Hash, parent common.Hash) error { // Dereference removes an existing reference from a root node. It's only // supported by hash-based database and will return an error for others. func (db *Database) Dereference(root common.Hash) error { - hdb, ok := db.backend.(*hashdb.Database) + hdb, ok := db.backend.(database.HashBackend) if !ok { return errors.New("not supported") } @@ -261,7 +271,7 @@ func (db *Database) Dereference(root common.Hash) error { // corresponding trie histories are existent. It's only supported by path-based // database and will return an error for others. func (db *Database) Recover(target common.Hash) error { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return errors.New("not supported") } @@ -279,7 +289,7 @@ func (db *Database) Recover(target common.Hash) error { // recovered. It's only supported by path-based database and will return an // error for others. func (db *Database) Recoverable(root common.Hash) (bool, error) { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return false, errors.New("not supported") } @@ -292,7 +302,7 @@ func (db *Database) Recoverable(root common.Hash) (bool, error) { // // It's only supported by path-based database and will return an error for others. func (db *Database) Disable() error { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return errors.New("not supported") } @@ -302,7 +312,7 @@ func (db *Database) Disable() error { // Enable activates database and resets the state tree with the provided persistent // state root once the state sync is finished. func (db *Database) Enable(root common.Hash) error { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return errors.New("not supported") } @@ -314,7 +324,7 @@ func (db *Database) Enable(root common.Hash) error { // flattening everything down (bad for reorgs). It's only supported by path-based // database and will return an error for others. func (db *Database) Journal(root common.Hash) error { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return errors.New("not supported") } @@ -325,7 +335,7 @@ func (db *Database) Journal(root common.Hash) error { // It's only supported by path-based database and will return an error for // others. func (db *Database) SetBufferSize(size int) error { - pdb, ok := db.backend.(*pathdb.Database) + pdb, ok := db.backend.(database.PathBackend) if !ok { return errors.New("not supported") } diff --git a/triedb/database/database.go b/triedb/database/database.go index 44d841016ba..14ea3b2887c 100644 --- a/triedb/database/database.go +++ b/triedb/database/database.go @@ -18,6 +18,8 @@ package database import ( "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/trie/trienode" + "github.com/ava-labs/libevm/trie/triestate" ) // Reader wraps the Node method of a backing trie reader. @@ -46,3 +48,59 @@ type Database interface { // An error will be returned if the specified state is not available. Reader(stateRoot common.Hash) (Reader, error) } + +// 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 + + // Size returns the current storage size of the diff layers on top of the + // disk layer and the storage size of the nodes cached in the disk layer. + // + // For hash scheme, there is no differentiation between diff layer nodes + // and dirty disk layer nodes, so both are merged into the second return. + Size() (common.StorageSize, common.StorageSize) + + // Update performs a state transition by committing dirty nodes contained + // in the given set in order to update state from the specified parent to + // the specified root. + // + // The passed in maps(nodes, states) will be retained to avoid copying + // everything. Therefore, these maps must not be changed afterwards. + Update(root common.Hash, parent common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *triestate.Set) error + + // Commit writes all relevant trie nodes belonging to the specified state + // to disk. Report specifies whether logs will be displayed in info level. + Commit(root common.Hash, report bool) error + + // Close closes the trie database backend and releases all held resources. + Close() error + + // Reader returns a node reader associated with the specific state. + // An error will be returned if the specified state is not available. + Reader(stateRoot common.Hash) (Reader, error) +} + +type HashBackend interface { + Backend + + Cap(limit common.StorageSize) error + Reference(root common.Hash, parent common.Hash) + Dereference(root common.Hash) +} + +type PathBackend interface { + Backend + + Recover(root common.Hash, loader triestate.TrieLoader) error + Recoverable(root common.Hash) bool + Disable() error + Enable(root common.Hash) error + Journal(root common.Hash) error + SetBufferSize(size int) error +} diff --git a/triedb/hashdb/database.go b/triedb/hashdb/database.go index 4727fc0e89c..a1e7a00d85e 100644 --- a/triedb/hashdb/database.go +++ b/triedb/hashdb/database.go @@ -33,6 +33,7 @@ import ( "github.com/ava-labs/libevm/rlp" "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/trie/triestate" + "github.com/ava-labs/libevm/triedb/database" ) var ( @@ -79,6 +80,10 @@ var Defaults = &Config{ CleanCacheSize: 0, } +func (c *Config) New(diskdb ethdb.Database, resolver ChildResolver) database.HashBackend { + return New(diskdb, c, resolver) +} + // Database is an intermediate write layer between the trie data structures and // the disk database. The aim is to accumulate trie writes in-memory and only // periodically flush a couple tries to disk, garbage collecting the remainder. @@ -631,7 +636,7 @@ func (db *Database) Scheme() string { // 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) { +func (db *Database) Reader(root common.Hash) (database.Reader, error) { if _, err := db.node(root); err != nil { return nil, fmt.Errorf("state %#x is not available, %v", root, err) } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 6c0c60c7697..0ed0ebac2e2 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -31,6 +31,7 @@ import ( "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/trie/triestate" + "github.com/ava-labs/libevm/triedb/database" ) const ( @@ -92,6 +93,10 @@ type Config struct { ReadOnly bool // Flag whether the database is opened in read only mode. } +func (c *Config) New(diskdb ethdb.Database) database.PathBackend { + return New(diskdb, c) +} + // sanitize checks the provided user configurations and changes anything that's // unreasonable or unworkable. func (c *Config) sanitize() *Config { @@ -208,7 +213,7 @@ func New(diskdb ethdb.Database, config *Config) *Database { } // Reader retrieves a layer belonging to the given state root. -func (db *Database) Reader(root common.Hash) (layer, error) { +func (db *Database) Reader(root common.Hash) (database.Reader, error) { l := db.tree.get(root) if l == nil { return nil, fmt.Errorf("state %#x is not available", root)