diff --git a/core/state/state.libevm.go b/core/state/state.libevm.go new file mode 100644 index 000000000000..c08a6a9b5fa4 --- /dev/null +++ b/core/state/state.libevm.go @@ -0,0 +1,64 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 state + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// GetExtra returns the extra payload from the [types.StateAccount] associated +// with the address, or a zero-value `SA` if not found. The +// [types.ExtraPayloads] MUST be sourced from [types.RegisterExtras]. +func GetExtra[SA any](s *StateDB, p types.ExtraPayloads[SA], addr common.Address) SA { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return p.FromStateAccount(&stateObject.data) + } + var zero SA + return zero +} + +// SetExtra sets the extra payload for the address. See [GetExtra] for details. +func SetExtra[SA any](s *StateDB, p types.ExtraPayloads[SA], addr common.Address, extra SA) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + setExtraOnObject(stateObject, p, addr, extra) + } +} + +func setExtraOnObject[SA any](s *stateObject, p types.ExtraPayloads[SA], addr common.Address, extra SA) { + s.db.journal.append(extraChange[SA]{ + payloads: p, + account: &addr, + prev: p.FromStateAccount(&s.data), + }) + p.SetOnStateAccount(&s.data, extra) +} + +// extraChange is a [journalEntry] for [SetExtra] / [setExtraOnObject]. +type extraChange[SA any] struct { + payloads types.ExtraPayloads[SA] + account *common.Address + prev SA +} + +func (e extraChange[SA]) dirtied() *common.Address { return e.account } + +func (e extraChange[SA]) revert(s *StateDB) { + e.payloads.SetOnStateAccount(&s.getStateObject(*e.account).data, e.prev) +} diff --git a/core/state/state.libevm_test.go b/core/state/state.libevm_test.go new file mode 100644 index 000000000000..696d527c0b33 --- /dev/null +++ b/core/state/state.libevm_test.go @@ -0,0 +1,172 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 state_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/libevm/ethtest" + "github.com/ethereum/go-ethereum/triedb" +) + +func TestGetSetExtra(t *testing.T) { + type accountExtra struct { + // Data is a pointer to test deep copying. + Data *[]byte // MUST be exported; I spent 20 minutes investigating failing tests because I'm an idiot + } + + types.TestOnlyClearRegisteredExtras() + t.Cleanup(types.TestOnlyClearRegisteredExtras) + // Just as its Data field is a pointer, the registered type is a pointer to + // test deep copying. + payloads := types.RegisterExtras[*accountExtra]() + + rng := ethtest.NewPseudoRand(42) + addr := rng.Address() + nonce := rng.Uint64() + balance := rng.Uint256() + buf := rng.Bytes(8) + extra := &accountExtra{Data: &buf} + + views := newWithSnaps(t) + stateDB := views.newStateDB(t, types.EmptyRootHash) + + assert.Nilf(t, state.GetExtra(stateDB, payloads, addr), "state.GetExtra() returns zero-value %T if before account creation", extra) + stateDB.CreateAccount(addr) + stateDB.SetNonce(addr, nonce) + stateDB.SetBalance(addr, balance) + assert.Nilf(t, state.GetExtra(stateDB, payloads, addr), "state.GetExtra() returns zero-value %T if after account creation but before SetExtra()", extra) + state.SetExtra(stateDB, payloads, addr, extra) + require.Equal(t, extra, state.GetExtra(stateDB, payloads, addr), "state.GetExtra() immediately after SetExtra()") + + root, err := stateDB.Commit(1, false) // arbitrary block number + require.NoErrorf(t, err, "%T.Commit(1, false)", stateDB) + require.NotEqualf(t, types.EmptyRootHash, root, "root hash returned by %T.Commit() is not the empty root", stateDB) + + t.Run(fmt.Sprintf("retrieve from %T", views.snaps), func(t *testing.T) { + iter, err := views.snaps.AccountIterator(root, common.Hash{}) + require.NoErrorf(t, err, "%T.AccountIterator(...)", views.snaps) + defer iter.Release() + + require.Truef(t, iter.Next(), "%T.Next() (i.e. at least one account)", iter) + require.NoErrorf(t, iter.Error(), "%T.Error()", iter) + + t.Run("types.FullAccount()", func(t *testing.T) { + got, err := types.FullAccount(iter.Account()) + require.NoErrorf(t, err, "types.FullAccount(%T.Account())", iter) + + want := &types.StateAccount{ + Nonce: nonce, + Balance: balance, + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + } + payloads.SetOnStateAccount(want, extra) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("types.FullAccount(%T.Account()) diff (-want +got):\n%s", iter, diff) + } + }) + + require.Falsef(t, iter.Next(), "%T.Next() after first account (i.e. only one)", iter) + }) + + t.Run(fmt.Sprintf("retrieve from new %T", stateDB), func(t *testing.T) { + s := views.newStateDB(t, root) + assert.Equalf(t, nonce, s.GetNonce(addr), "%T.GetNonce()", s) + assert.Equalf(t, balance, s.GetBalance(addr), "%T.GetBalance()", s) + assert.Equal(t, extra, state.GetExtra(s, payloads, addr), "state.GetExtra()") + }) + + t.Run("reverting to snapshot", func(t *testing.T) { + s := views.newStateDB(t, root) + snap := s.Snapshot() + + oldExtra := extra + buf := append(*oldExtra.Data, rng.Bytes(8)...) + newExtra := &accountExtra{Data: &buf} + + state.SetExtra(s, payloads, addr, newExtra) + assert.Equalf(t, newExtra, state.GetExtra(s, payloads, addr), "state.GetExtra() after overwriting with new value") + s.RevertToSnapshot(snap) + assert.Equalf(t, oldExtra, state.GetExtra(s, payloads, addr), "state.GetExtra() after reverting to snapshot") + }) + + t.Run(fmt.Sprintf("%T.Copy()", stateDB), func(t *testing.T) { + require.Equalf(t, reflect.Pointer, reflect.TypeOf(extra).Kind(), "extra-payload type") + require.Equalf(t, reflect.Pointer, reflect.TypeOf(extra.Data).Kind(), "extra-payload field") + + orig := views.newStateDB(t, root) + cp := orig.Copy() + + oldExtra := extra + buf := append(*oldExtra.Data, rng.Bytes(8)...) + newExtra := &accountExtra{Data: &buf} + + assert.Equalf(t, oldExtra, state.GetExtra(orig, payloads, addr), "GetExtra([original %T]) before setting", orig) + assert.Equalf(t, oldExtra, state.GetExtra(cp, payloads, addr), "GetExtra([copy of %T]) returns the same payload", orig) + state.SetExtra(orig, payloads, addr, newExtra) + assert.Equalf(t, newExtra, state.GetExtra(orig, payloads, addr), "GetExtra([original %T]) returns overwritten payload", orig) + assert.Equalf(t, oldExtra, state.GetExtra(cp, payloads, addr), "GetExtra([copy of %T]) returns original payload despite overwriting on original", orig) + }) +} + +// stateViews are different ways to access the same data. +type stateViews struct { + snaps *snapshot.Tree + database state.Database +} + +func (v stateViews) newStateDB(t *testing.T, root common.Hash) *state.StateDB { + t.Helper() + s, err := state.New(root, v.database, v.snaps) + require.NoError(t, err, "state.New()") + return s +} + +func newWithSnaps(t *testing.T) stateViews { + t.Helper() + empty := types.EmptyRootHash + kvStore := memorydb.New() + ethDB := rawdb.NewDatabase(kvStore) + snaps, err := snapshot.New( + snapshot.Config{ + CacheSize: 16, // Mb (arbitrary but non-zero) + }, + kvStore, + triedb.NewDatabase(ethDB, nil), + empty, + ) + require.NoError(t, err, "snapshot.New()") + + return stateViews{ + snaps: snaps, + database: state.NewDatabase(ethDB), + } +} diff --git a/core/state/state_object.go b/core/state/state_object.go index fc26af68dbe7..2b8c0255ce7b 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -93,7 +93,7 @@ type stateObject struct { // empty returns whether the account is considered empty. func (s *stateObject) empty() bool { - return s.data.Nonce == 0 && s.data.Balance.IsZero() && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes()) + return s.data.Nonce == 0 && s.data.Balance.IsZero() && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes()) && s.data.Extra.IsZero() } // newObject creates a state object. diff --git a/core/state/statedb.go b/core/state/statedb.go index 8092155ce595..04f6e684ae79 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/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index 3a2957edd14f..f37fb8cbee00 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -207,3 +207,7 @@ func (e *StateAccountExtra) Format(s fmt.State, verb rune) { } _, _ = s.Write([]byte(out)) } + +func (e *StateAccountExtra) IsZero() bool { + return e == nil || e.t == nil || e.t.IsZero() +} diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index d7e10173cf27..0421a45e3533 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -290,6 +290,16 @@ func (t *prestateTracer) lookupAccount(addr common.Address) { // it to the prestate of the given contract. It assumes `lookupAccount` // has been performed on the contract before. func (t *prestateTracer) lookupStorage(addr common.Address, key common.Hash) { + // TODO: Refactor coreth test outside of eth/tracers/internal + // See https://github.com/ava-labs/coreth/pull/286 for context. + // lookupStorage assumes that lookupAccount has already been called. + // This assumption is violated for some historical blocks by the NativeAssetCall + // precompile. To fix this, we perform an extra call to lookupAccount here to ensure + // that the pre-state account is populated before attempting to read from the Storage + // map. When the invariant is maintained properly (since de-activation of the precompile), + // lookupAccount is a no-op. When the invariant is broken by the precompile, this avoids + // the panic and correctly captures the account prestate before the next opcode is executed. + t.lookupAccount(addr) if _, ok := t.pre[addr].Storage[key]; ok { return } diff --git a/go.mod b/go.mod index 7a54b1ff7ca9..af6b9a0b399f 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/protobuf v1.5.3 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb + github.com/google/go-cmp v0.5.9 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 @@ -106,7 +107,6 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/params/config.go b/params/config.go index 2762a504c44b..0ed94cd2f9f9 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 bf8ce2fc5e24..fa444a1d0b76 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/ethereum/go-ethereum/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 939a21f1478b..3fb82f31b816 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -27,15 +27,14 @@ import ( "github.com/ethereum/go-ethereum/trie/triestate" "github.com/ethereum/go-ethereum/triedb/database" "github.com/ethereum/go-ethereum/triedb/hashdb" - "github.com/ethereum/go-ethereum/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 18a8f454e2f4..95a54fd53dad 100644 --- a/triedb/database/database.go +++ b/triedb/database/database.go @@ -18,6 +18,8 @@ package database import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/ethereum/go-ethereum/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 e45ccdba32ca..671edd8e997b 100644 --- a/triedb/hashdb/database.go +++ b/triedb/hashdb/database.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" + "github.com/ethereum/go-ethereum/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 f2d6cea635a9..c5e2ecff2303 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" + "github.com/ethereum/go-ethereum/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)