diff --git a/core/state/dump.go b/core/state/dump.go index 55abb50f1c5a..1dc02f138b98 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -54,6 +54,7 @@ type DumpAccount struct { Root hexutil.Bytes `json:"root"` CodeHash hexutil.Bytes `json:"codeHash"` Code hexutil.Bytes `json:"code,omitempty"` + IsMultiCoin bool `json:"isMultiCoin,omitempty"` Storage map[common.Hash]string `json:"storage,omitempty"` Address *common.Address `json:"address,omitempty"` // Address only present in iterative (line-by-line) mode AddressHash hexutil.Bytes `json:"key,omitempty"` // If we don't have address, we can output the key @@ -97,6 +98,7 @@ func (d iterativeDump) OnAccount(addr *common.Address, account DumpAccount) { Root: account.Root, CodeHash: account.CodeHash, Code: account.Code, + IsMultiCoin: account.IsMultiCoin, Storage: account.Storage, AddressHash: account.AddressHash, Address: addr, @@ -144,6 +146,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] Nonce: data.Nonce, Root: data.Root[:], CodeHash: data.CodeHash, + IsMultiCoin: data.IsMultiCoin, AddressHash: it.Key, } address *common.Address diff --git a/core/state/interfaces.go b/core/state/interfaces.go new file mode 100644 index 000000000000..5c42ae175722 --- /dev/null +++ b/core/state/interfaces.go @@ -0,0 +1,35 @@ +package state + +import ( + "reflect" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/snapshot" +) + +type SnapshotTree interface { + Snapshot(common.Hash) snapshot.Snapshot + UpdateWithBlockHash( + root common.Hash, + parent common.Hash, + blockHash common.Hash, + parentHash common.Hash, + destructs map[common.Hash]struct{}, + accounts map[common.Hash][]byte, + storage map[common.Hash]map[common.Hash][]byte, + ) error +} + +// https://vitaneri.com/posts/check-for-nil-interface-in-go +func checkNilInterface(i interface{}) bool { + iv := reflect.ValueOf(i) + if !iv.IsValid() { + return true + } + switch iv.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Func, reflect.Interface: + return iv.IsNil() + default: + return false + } +} diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index 6389842382ed..482fe91f46f9 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -112,6 +112,12 @@ type Snapshot interface { // Storage directly retrieves the storage data associated with a particular hash, // within a particular account. Storage(accountHash, storageHash common.Hash) ([]byte, error) + + // AccountIterator creates an account iterator over an arbitrary layer. + AccountIterator(seek common.Hash) AccountIterator + + // StorageIterator creates a storage iterator over an arbitrary layer. + StorageIterator(account common.Hash, seek common.Hash) (StorageIterator, bool) } // snapshot is the internal version of the snapshot data layer that supports some @@ -140,12 +146,6 @@ type snapshot interface { // Stale return whether this layer has become stale (was flattened across) or // if it's still live. Stale() bool - - // AccountIterator creates an account iterator over an arbitrary layer. - AccountIterator(seek common.Hash) AccountIterator - - // StorageIterator creates a storage iterator over an arbitrary layer. - StorageIterator(account common.Hash, seek common.Hash) (StorageIterator, bool) } // Config includes the configurations for snapshots. @@ -338,6 +338,32 @@ func (t *Tree) Snapshots(root common.Hash, limits int, nodisk bool) []Snapshot { return ret } +func (t *Tree) UpdateWithBlockHash( + root common.Hash, + parent common.Hash, + blockHash common.Hash, + parentHash common.Hash, + destructs map[common.Hash]struct{}, + accounts map[common.Hash][]byte, + storage map[common.Hash]map[common.Hash][]byte, +) error { + // Only update if there's a state transition (skip empty Clique blocks) + if parent != root { + if err := t.Update(root, parent, destructs, accounts, storage); err != nil { + log.Warn("Failed to update snapshot tree", "from", parent, "to", root, "err", err) + } + + // Keep 128 diff layers in the memory, persistent layer is 129th. + // - head layer is paired with HEAD state + // - head-1 layer is paired with HEAD-1 state + // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state + if err := t.Cap(root, 128); err != nil { + log.Warn("Failed to cap snapshot tree", "root", root, "layers", 128, "err", err) + } + } + return nil +} + // Update adds a new snapshot into the tree, if that can be linked to an existing // old parent. It is disallowed to insert a disk layer (the origin of all). func (t *Tree) 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 { diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go index b66799757e19..40863fa4dedf 100644 --- a/core/state/snapshot/snapshot_test.go +++ b/core/state/snapshot/snapshot_test.go @@ -119,7 +119,7 @@ func TestDiskLayerExternalInvalidationFullFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -169,7 +169,7 @@ func TestDiskLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -231,7 +231,7 @@ func TestDiffLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the accumulator diff layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -280,7 +280,7 @@ func TestPostCapBasicDataAccess(t *testing.T) { // shouldErr checks that an account access errors as expected shouldErr := func(layer *diffLayer, key string) error { if data, err := layer.Account(common.HexToHash(key)); err == nil { - return fmt.Errorf("expected error, got data %x", data) + return fmt.Errorf("expected error, got data %v", data) } return nil } diff --git a/core/state/state_object.go b/core/state/state_object.go index 9383b98e4497..bd18848a8bb4 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.Sign() == 0 && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes()) + return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes()) && !s.data.IsMultiCoin } // newObject creates a state object. diff --git a/core/state/state_object_coreth.go b/core/state/state_object_coreth.go new file mode 100644 index 000000000000..713008a42ced --- /dev/null +++ b/core/state/state_object_coreth.go @@ -0,0 +1,82 @@ +package state + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// AddBalanceMultiCoin adds amount of coinID to s's balance. +// It is used to add multicoin funds to the destination account of a transfer. +func (s *stateObject) AddBalanceMultiCoin(coinID common.Hash, amount *big.Int, db Database) { + if amount.Sign() == 0 { + if s.empty() { + s.touch() + } + + return + } + s.SetBalanceMultiCoin(coinID, new(big.Int).Add(s.BalanceMultiCoin(coinID, db), amount), db) +} + +// SubBalanceMultiCoin removes amount of coinID from s's balance. +// It is used to remove multicoin funds from the origin account of a transfer. +func (s *stateObject) SubBalanceMultiCoin(coinID common.Hash, amount *big.Int, db Database) { + if amount.Sign() == 0 { + return + } + s.SetBalanceMultiCoin(coinID, new(big.Int).Sub(s.BalanceMultiCoin(coinID, db), amount), db) +} + +func (s *stateObject) SetBalanceMultiCoin(coinID common.Hash, amount *big.Int, db Database) { + s.EnableMultiCoin() + NormalizeCoinID(&coinID) + s.SetState(coinID, common.BigToHash(amount)) +} +func (s *stateObject) enableMultiCoin() { + s.data.IsMultiCoin = true +} + +// NormalizeCoinID ORs the 0th bit of the first byte in +// [coinID], which ensures this bit will be 1 and all other +// bits are left the same. +// This partitions multicoin storage from normal state storage. +func NormalizeCoinID(coinID *common.Hash) { + coinID[0] |= 0x01 +} + +// NormalizeStateKey ANDs the 0th bit of the first byte in +// [key], which ensures this bit will be 0 and all other bits +// are left the same. +// This partitions normal state storage from multicoin storage. +func NormalizeStateKey(key *common.Hash) { + key[0] &= 0xfe +} + +func (s *stateObject) BalanceMultiCoin(coinID common.Hash, db Database) *big.Int { + NormalizeCoinID(&coinID) + return s.GetState(coinID).Big() +} + +func (s *stateObject) EnableMultiCoin() bool { + if s.data.IsMultiCoin { + return false + } + s.db.journal.append(multiCoinEnable{ + account: &s.address, + }) + s.enableMultiCoin() + return true +} + +type multiCoinEnable struct { + account *common.Address +} + +func (ch multiCoinEnable) revert(s *StateDB) { + s.getStateObject(*ch.account).data.IsMultiCoin = false +} + +func (ch multiCoinEnable) dirtied() *common.Address { + return ch.account +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 905944cbb5b9..855b309beeb5 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -63,7 +63,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 +141,17 @@ 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 || checkNilInterface(snaps) { + return NewWithSnapshot(root, db, nil, nil) + } + return NewWithSnapshot(root, db, snaps, snaps.Snapshot(root)) +} + +// NewWithSnapshot creates a new state from a given trie with the specified [snap] +// If [snap] doesn't have the same root as [root], then NewWithSnapshot will return +// an error. +func NewWithSnapshot(root common.Hash, db Database, snaps SnapshotTree, snap snapshot.Snapshot) (*StateDB, error) { tr, err := db.OpenTrie(root) if err != nil { return nil, err @@ -166,8 +176,11 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) transientStorage: newTransientStorage(), hasher: crypto.NewKeccakState(), } - if sdb.snaps != nil { - sdb.snap = sdb.snaps.Snapshot(root) + if snap != nil { + if snap.Root() != root { + return nil, fmt.Errorf("cannot create new statedb for root: %s, using snapshot with mismatched root: %s", root, snap.Root().Hex()) + } + sdb.snap = snap } return sdb, nil } @@ -257,11 +270,13 @@ func (s *StateDB) AddRefund(gas uint64) { } // SubRefund removes gas from the refund counter. -// This method will panic if the refund counter goes below zero +// This method will set the refund counter to 0 if the gas is greater than the current refund. func (s *StateDB) SubRefund(gas uint64) { s.journal.append(refundChange{prev: s.refund}) if gas > s.refund { - panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund)) + log.Warn("Setting refund to 0", "currentRefund", s.refund, "gas", gas) + s.refund = 0 + return } s.refund -= gas } @@ -285,7 +300,7 @@ func (s *StateDB) GetBalance(addr common.Address) *big.Int { if stateObject != nil { return stateObject.Balance() } - return common.Big0 + return new(big.Int).Set(common.Big0) } // GetNonce retrieves the nonce from the given address or 0 if object not found @@ -566,19 +581,18 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { var data *types.StateAccount if s.snap != nil { start := time.Now() - acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) + acc, err := s.snap.AccountRLP(crypto.HashData(s.hasher, addr.Bytes())) if metrics.EnabledExpensive { s.SnapshotAccountReads += time.Since(start) } if err == nil { - if acc == nil { + if len(acc) == 0 { return nil } - data = &types.StateAccount{ - Nonce: acc.Nonce, - Balance: acc.Balance, - CodeHash: acc.CodeHash, - Root: common.BytesToHash(acc.Root), + data, err = types.FullAccount(acc) + if err != nil { + s.setError(fmt.Errorf("could not unmarshal account (%x) error: %w", addr.Bytes(), err)) + return nil } if len(data.CodeHash) == 0 { data.CodeHash = types.EmptyCodeHash.Bytes() @@ -950,10 +964,7 @@ func (s *StateDB) clearJournalAndRefund() { // storage iteration and constructs trie node deletion markers by creating // stack trie with iterated slots. func (s *StateDB) fastDeleteStorage(addrHash common.Hash, root common.Hash) (bool, common.StorageSize, map[common.Hash][]byte, *trienode.NodeSet, error) { - iter, err := s.snaps.StorageIterator(s.originalRoot, addrHash, common.Hash{}) - if err != nil { - return false, 0, nil, nil, err - } + iter, _ := s.snap.StorageIterator(addrHash, common.Hash{}) defer iter.Release() var ( @@ -1154,6 +1165,15 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A } // Commit writes the state to the underlying in-memory trie database. +func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, error) { + return s.commit(block, deleteEmptyObjects, common.Hash{}, common.Hash{}) +} + +// CommitWithBlockHash writes the state to the underlying in-memory trie database. +func (s *StateDB) CommitWithBlockHash(block uint64, deleteEmptyObjects bool, blockHash, parentHash common.Hash) (common.Hash, error) { + return s.commit(block, deleteEmptyObjects, blockHash, parentHash) +} + // Once the state is committed, tries cached in stateDB (including account // trie, storage tries) will no longer be functional. A new state instance // must be created with new root and updated database for accessing post- @@ -1161,7 +1181,7 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A // // The associated block number of the state transition is also provided // for more chain context. -func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, error) { +func (s *StateDB) commit(block uint64, deleteEmptyObjects bool, blockHash, parentHash common.Hash) (common.Hash, error) { // Short circuit in case any database failure occurred earlier. if s.dbErr != nil { return common.Hash{}, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr) @@ -1249,18 +1269,13 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er // If snapshotting is enabled, update the snapshot tree with this new version if s.snap != nil { start := time.Now() - // Only update if there's a state transition (skip empty Clique blocks) - if parent := s.snap.Root(); parent != root { - if err := s.snaps.Update(root, parent, s.convertAccountSet(s.stateObjectsDestruct), s.accounts, s.storages); err != nil { - log.Warn("Failed to update snapshot tree", "from", parent, "to", root, "err", err) - } - // Keep 128 diff layers in the memory, persistent layer is 129th. - // - head layer is paired with HEAD state - // - head-1 layer is paired with HEAD-1 state - // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state - if err := s.snaps.Cap(root, 128); err != nil { - log.Warn("Failed to cap snapshot tree", "root", root, "layers", 128, "err", err) - } + if err := s.snaps.UpdateWithBlockHash( + root, s.snap.Root(), + blockHash, parentHash, + s.convertAccountSet(s.stateObjectsDestruct), + s.accounts, s.storages, + ); err != nil { + log.Warn("Failed to update snapshot tree", "from", s.snap.Root(), "to", root, "err", err) } if metrics.EnabledExpensive { s.SnapshotCommits += time.Since(start) @@ -1274,7 +1289,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er if origin == (common.Hash{}) { origin = types.EmptyRootHash } - if root != origin { + { start := time.Now() set := triestate.New(s.accountsOrigin, s.storagesOrigin, incomplete) if err := s.db.TrieDB().Update(root, origin, block, nodes, set); err != nil { diff --git a/core/state/statedb_coreth.go b/core/state/statedb_coreth.go new file mode 100644 index 000000000000..dcfb8257fc2d --- /dev/null +++ b/core/state/statedb_coreth.go @@ -0,0 +1,49 @@ +package state + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// Retrieve the balance from the given address or 0 if object not found +func (s *StateDB) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.BalanceMultiCoin(coinID, s.db) + } + return new(big.Int).Set(common.Big0) +} + +// GetCommittedStateAP1 retrieves a value from the given account's committed storage trie. +func (s *StateDB) GetCommittedStateAP1(addr common.Address, hash common.Hash) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + NormalizeStateKey(&hash) + return stateObject.GetCommittedState(hash) + } + return common.Hash{} +} + +// AddBalance adds amount to the account associated with addr. +func (s *StateDB) AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + stateObject.AddBalanceMultiCoin(coinID, amount, s.db) + } +} + +// SubBalance subtracts amount from the account associated with addr. +func (s *StateDB) SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + stateObject.SubBalanceMultiCoin(coinID, amount, s.db) + } +} + +func (s *StateDB) SetBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetBalanceMultiCoin(coinID, amount, s.db) + } +} diff --git a/core/state/statedb_ext.go b/core/state/statedb_ext.go new file mode 100644 index 000000000000..7fa7b0952e05 --- /dev/null +++ b/core/state/statedb_ext.go @@ -0,0 +1,24 @@ +package state + +import ( + "github.com/ethereum/go-ethereum/common" +) + +// GetTxHash returns the current tx hash on the StateDB set by SetTxContext. +func (s *StateDB) GetTxHash() common.Hash { + return s.thash +} + +// GetLogData returns the underlying topics and data from each log included in the StateDB +// Test helper function. +func (s *StateDB) GetLogData() ([][]common.Hash, [][]byte) { + var logData [][]byte + var topics [][]common.Hash + for _, lgs := range s.logs { + for _, log := range lgs { + topics = append(topics, log.Topics) + logData = append(logData, common.CopyBytes(log.Data)) + } + } + return topics, logData +} diff --git a/core/state_transition.go b/core/state_transition.go index 540f63fda7ea..9fada3041e16 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -182,6 +182,11 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err return NewStateTransition(evm, msg, gp).TransitionDb() } +type stateDB interface { + vm.StateDB + Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) +} + // StateTransition represents a state transition. // // == The State Transitioning Model @@ -209,7 +214,7 @@ type StateTransition struct { msg *Message gasRemaining uint64 initialGas uint64 - state vm.StateDB + state stateDB evm *vm.EVM } @@ -219,7 +224,7 @@ func NewStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *StateTransition gp: gp, evm: evm, msg: msg, - state: evm.StateDB, + state: evm.StateDB.(stateDB), } } diff --git a/core/types/account_rlp.go b/core/types/account_rlp.go new file mode 100644 index 000000000000..8e6ab937d618 --- /dev/null +++ b/core/types/account_rlp.go @@ -0,0 +1,144 @@ +package types + +import ( + "io" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" +) + +var MultiCoinEnabled = false + +func (obj *StateAccount) EncodeRLP(_w io.Writer) error { + w := rlp.NewEncoderBuffer(_w) + _tmp0 := w.List() + w.WriteUint64(obj.Nonce) + if obj.Balance == nil { + w.Write(rlp.EmptyString) + } else { + if obj.Balance.Sign() == -1 { + return rlp.ErrNegativeBigInt + } + w.WriteBigInt(obj.Balance) + } + w.WriteBytes(obj.Root[:]) + w.WriteBytes(obj.CodeHash) + if MultiCoinEnabled { + w.WriteBool(obj.IsMultiCoin) + } + w.ListEnd(_tmp0) + return w.Flush() +} + +func (obj *StateAccount) DecodeRLP(dec *rlp.Stream) error { + var _tmp0 StateAccount + { + if _, err := dec.List(); err != nil { + return err + } + // Nonce: + _tmp1, err := dec.Uint64() + if err != nil { + return err + } + _tmp0.Nonce = _tmp1 + // Balance: + _tmp2, err := dec.BigInt() + if err != nil { + return err + } + _tmp0.Balance = _tmp2 + // Root: + var _tmp3 common.Hash + if err := dec.ReadBytes(_tmp3[:]); err != nil { + return err + } + _tmp0.Root = _tmp3 + // CodeHash: + _tmp4, err := dec.Bytes() + if err != nil { + return err + } + _tmp0.CodeHash = _tmp4 + // IsMultiCoin: + if MultiCoinEnabled { + _tmp5, err := dec.Bool() + if err != nil { + return err + } + _tmp0.IsMultiCoin = _tmp5 + } + if err := dec.ListEnd(); err != nil { + return err + } + } + *obj = _tmp0 + return nil +} + +func (obj SlimAccount) EncodeRLP(_w io.Writer) error { + w := rlp.NewEncoderBuffer(_w) + _tmp0 := w.List() + w.WriteUint64(obj.Nonce) + if obj.Balance == nil { + w.Write(rlp.EmptyString) + } else { + if obj.Balance.Sign() == -1 { + return rlp.ErrNegativeBigInt + } + w.WriteBigInt(obj.Balance) + } + w.WriteBytes(obj.Root) + w.WriteBytes(obj.CodeHash) + if MultiCoinEnabled { + w.WriteBool(obj.IsMultiCoin) + } + w.ListEnd(_tmp0) + return w.Flush() +} + +func (obj *SlimAccount) DecodeRLP(dec *rlp.Stream) error { + var _tmp0 SlimAccount + { + if _, err := dec.List(); err != nil { + return err + } + // Nonce: + _tmp1, err := dec.Uint64() + if err != nil { + return err + } + _tmp0.Nonce = _tmp1 + // Balance: + _tmp2, err := dec.BigInt() + if err != nil { + return err + } + _tmp0.Balance = _tmp2 + // Root: + _tmp3, err := dec.Bytes() + if err != nil { + return err + } + _tmp0.Root = _tmp3 + // CodeHash: + _tmp4, err := dec.Bytes() + if err != nil { + return err + } + _tmp0.CodeHash = _tmp4 + if MultiCoinEnabled { + // IsMultiCoin: + _tmp5, err := dec.Bool() + if err != nil { + return err + } + _tmp0.IsMultiCoin = _tmp5 + } + if err := dec.ListEnd(); err != nil { + return err + } + } + *obj = _tmp0 + return nil +} diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go deleted file mode 100644 index 3fb36f403875..000000000000 --- a/core/types/gen_account_rlp.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by rlpgen. DO NOT EDIT. - -package types - -import "github.com/ethereum/go-ethereum/rlp" -import "io" - -func (obj *StateAccount) EncodeRLP(_w io.Writer) error { - w := rlp.NewEncoderBuffer(_w) - _tmp0 := w.List() - w.WriteUint64(obj.Nonce) - if obj.Balance == nil { - w.Write(rlp.EmptyString) - } else { - if obj.Balance.Sign() == -1 { - return rlp.ErrNegativeBigInt - } - w.WriteBigInt(obj.Balance) - } - w.WriteBytes(obj.Root[:]) - w.WriteBytes(obj.CodeHash) - w.ListEnd(_tmp0) - return w.Flush() -} diff --git a/core/types/state_account.go b/core/types/state_account.go index ad07ca3f3a3d..d6205e790466 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -24,8 +24,6 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -//go:generate go run ../../rlp/rlpgen -type StateAccount -out gen_account_rlp.go - // StateAccount is the Ethereum consensus representation of accounts. // These objects are stored in the main account trie. type StateAccount struct { @@ -33,6 +31,8 @@ type StateAccount struct { Balance *big.Int Root common.Hash // merkle root of the storage trie CodeHash []byte + + IsMultiCoin bool `rlp:"-"` // Coreth extension: Not encoded by default } // NewEmptyStateAccount constructs an empty state account. @@ -51,10 +51,11 @@ func (acct *StateAccount) Copy() *StateAccount { balance = new(big.Int).Set(acct.Balance) } return &StateAccount{ - Nonce: acct.Nonce, - Balance: balance, - Root: acct.Root, - CodeHash: common.CopyBytes(acct.CodeHash), + Nonce: acct.Nonce, + Balance: balance, + Root: acct.Root, + CodeHash: common.CopyBytes(acct.CodeHash), + IsMultiCoin: acct.IsMultiCoin, } } @@ -66,13 +67,16 @@ type SlimAccount struct { Balance *big.Int Root []byte // Nil if root equals to types.EmptyRootHash CodeHash []byte // Nil if hash equals to types.EmptyCodeHash + + IsMultiCoin bool `rlp:"-"` // Coreth extension: Not encoded by default } // SlimAccountRLP encodes the state account in 'slim RLP' format. func SlimAccountRLP(account StateAccount) []byte { slim := SlimAccount{ - Nonce: account.Nonce, - Balance: account.Balance, + Nonce: account.Nonce, + Balance: account.Balance, + IsMultiCoin: account.IsMultiCoin, } if account.Root != EmptyRootHash { slim.Root = account.Root[:] diff --git a/core/vm/contract_ext.go b/core/vm/contract_ext.go new file mode 100644 index 000000000000..7715b15a3928 --- /dev/null +++ b/core/vm/contract_ext.go @@ -0,0 +1,13 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +func (contract *Contract) AsGenesisContract() *Contract { + self := AccountRef(contract.Caller()) + if _, ok := contract.caller.(*Contract); ok { + contract = contract.AsDelegate() + } + contract.self = self + return contract +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 35f0a3f7c283..75a2a168516a 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -104,7 +104,7 @@ func enable1344(jt *JumpTable) { // opChainID implements CHAINID opcode func opChainID(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - chainId, _ := uint256.FromBig(interpreter.evm.chainConfig.ChainID) + chainId, _ := uint256.FromBig(interpreter.evm.chainRules.ChainID) scope.Stack.push(chainId) return nil, nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index 088b18aaa4ff..9534a1e28bf7 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -37,7 +37,13 @@ type ( GetHashFunc func(uint64) common.Hash ) -func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { +type ( + // RunFunc is the signature of a precompiled contract run function + // Consider passing caller as ContractRef instead of common.Address + RunFunc func(caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) +) + +func (evm *EVM) precompile(addr common.Address) (RunFunc, bool) { var precompiles map[common.Address]PrecompiledContract switch { case evm.chainRules.IsCancun: @@ -52,7 +58,15 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { precompiles = PrecompiledContractsHomestead } p, ok := precompiles[addr] - return p, ok + if ok { + runFn := func(caller common.Address, input []byte, suppliedGas uint64, readOnly bool) ([]byte, uint64, error) { + return RunPrecompiledContract(p, input, suppliedGas) + } + return runFn, ok + } + + runFn, ok := evm.Config.CustomPrecompiles[addr] + return runFn, ok } // BlockContext provides the EVM with auxiliary information. Once provided @@ -75,6 +89,8 @@ type BlockContext struct { BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO + + Extra interface{} } // TxContext provides the EVM with information about a transaction. @@ -106,7 +122,7 @@ type EVM struct { depth int // chainConfig contains information about the current chain - chainConfig *params.ChainConfig + chainConfig ChainConfig // chain rules contains the chain rules for the current epoch chainRules params.Rules // virtual machine configuration options used to initialise the @@ -123,9 +139,16 @@ type EVM struct { callGasTemp uint64 } +type ChainConfig interface { + Rules(blockNum *big.Int, isMerge bool, time uint64) params.Rules + IsLondon(blockNum *big.Int) bool + IsEIP158(blockNum *big.Int) bool + IsCancun(blockNum *big.Int, time uint64) bool +} + // NewEVM returns a new EVM. The returned EVM is not thread safe and should // only ever be used *once*. -func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM { +func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig ChainConfig, config Config) *EVM { // If basefee tracking is disabled (eth_call, eth_estimateGas, etc), and no // gas prices were specified, lower the basefee to 0 to avoid breaking EVM // invariants (basefee < feecap) @@ -224,7 +247,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p(caller.Address(), input, gas, evm.interpreter.readOnly) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -287,7 +310,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p(caller.Address(), input, gas, evm.interpreter.readOnly) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -332,7 +355,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p(caller.Address(), input, gas, evm.interpreter.readOnly) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -381,7 +404,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte } if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = p(caller.Address(), input, gas, evm.interpreter.readOnly) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' @@ -428,6 +451,13 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { return nil, common.Address{}, gas, ErrInsufficientBalance } + // If there is any collision with a prohibited address, return an error instead + // of allowing the contract to be created. + if isProhibited := evm.Config.IsProhibited; isProhibited != nil { + if err := isProhibited(address); err != nil { + return nil, common.Address{}, gas, err + } + } nonce := evm.StateDB.GetNonce(caller.Address()) if nonce+1 < nonce { return nil, common.Address{}, gas, ErrNonceUintOverflow @@ -443,6 +473,13 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) { return nil, common.Address{}, 0, ErrContractAddressCollision } + // Subnet-EVM extension to control contract creation + if evm.Config.CanDeploy != nil { + origin := evm.TxContext.Origin + if err := evm.Config.CanDeploy(origin); err != nil { + return nil, common.Address{}, 0, err + } + } // Create a new account on the state snapshot := evm.StateDB.Snapshot() evm.StateDB.CreateAccount(address) @@ -526,4 +563,4 @@ func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment * } // ChainConfig returns the environment's chain configuration -func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } +func (evm *EVM) ChainConfig() ChainConfig { return evm.chainConfig } diff --git a/core/vm/evm_ext.go b/core/vm/evm_ext.go new file mode 100644 index 000000000000..b85728599dac --- /dev/null +++ b/core/vm/evm_ext.go @@ -0,0 +1,25 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +func (bc *BlockContext) Number() *big.Int { + return bc.BlockNumber +} + +func (bc *BlockContext) Timestamp() uint64 { + return bc.Time +} + +func (evm *EVM) ActivePrecompiles() []common.Address { + if evm.Config.ActivePrecompiles != nil { + return evm.Config.ActivePrecompiles + } + return ActivePrecompiles(evm.chainRules) +} diff --git a/core/vm/gas_table_ext.go b/core/vm/gas_table_ext.go new file mode 100644 index 000000000000..9409ebb8d5a8 --- /dev/null +++ b/core/vm/gas_table_ext.go @@ -0,0 +1,186 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/params" +) + +type withGetCommittedStateAP1 interface { + GetCommittedStateAP1(address common.Address, key common.Hash) common.Hash +} + +// gasSStoreEIP2929 implements gas cost for SSTORE according to EIP-2929 +// +// When calling SSTORE, check if the (address, storage_key) pair is in accessed_storage_keys. +// If it is not, charge an additional COLD_SLOAD_COST gas, and add the pair to accessed_storage_keys. +// Additionally, modify the parameters defined in EIP 2200 as follows: +// +// Parameter Old value New value +// SLOAD_GAS 800 = WARM_STORAGE_READ_COST +// SSTORE_RESET_GAS 5000 5000 - COLD_SLOAD_COST +// +// The other parameters defined in EIP 2200 are unchanged. +// see gasSStoreEIP2200(...) in core/vm/gas_table.go for more info about how EIP 2200 is specified +func _gasSStoreEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + // If we fail the minimum gas availability invariant, fail (0) + if contract.Gas <= params.SstoreSentryGasEIP2200 { + return 0, errors.New("not enough gas for reentrancy sentry") + } + // Gas sentry honoured, do the actual gas calculation based on the stored value + var ( + y, x = stack.Back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + current = evm.StateDB.GetState(contract.Address(), slot) + cost = uint64(0) + ) + // Check slot presence in the access list + if addrPresent, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + cost = params.ColdSloadCostEIP2929 + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + if !addrPresent { + // Once we're done with YOLOv2 and schedule this for mainnet, might + // be good to remove this panic here, which is just really a + // canary to have during testing + panic("impossible case: address was not present in access list during sstore op") + } + } + value := common.Hash(y.Bytes32()) + + if current == value { // noop (1) + // EIP 2200 original clause: + // return params.SloadGasEIP2200, nil + return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS + } + original := evm.StateDB.(withGetCommittedStateAP1).GetCommittedStateAP1(contract.Address(), x.Bytes32()) + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return cost + params.SstoreSetGasEIP2200, nil + } + // EIP-2200 original clause: + // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2) + } + + // EIP-2200 original clause: + //return params.SloadGasEIP2200, nil // dirty update (2.2) + return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2) +} + +func _gasSelfdestructEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + var ( + gas uint64 + address = common.Address(stack.peek().Bytes20()) + ) + if !evm.StateDB.AddressInAccessList(address) { + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddAddressToAccessList(address) + gas = params.ColdAccountAccessCostEIP2929 + } + // if empty and transfers value + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas += params.CreateBySelfdestructGas + } + + return gas, nil +} + +// gasSStoreAP1 simplifies the dynamic gas cost of SSTORE by removing all refund logic +// +// 0. If *gasleft* is less than or equal to 2300, fail the current call. +// 1. If current value equals new value (this is a no-op), SLOAD_GAS is deducted. +// 2. If current value does not equal new value: +// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context): +// 2.1.1. If original value is 0, SSTORE_SET_GAS (20K) gas is deducted. +// 2.1.2. Otherwise, SSTORE_RESET_GAS gas is deducted. If new value is 0, add SSTORE_CLEARS_SCHEDULE to refund counter. +// 2.2. If original value does not equal current value (this storage slot is dirty), SLOAD_GAS gas is deducted. Apply both of the following clauses: +func gasSStoreAP1(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + // If we fail the minimum gas availability invariant, fail (0) + if contract.Gas <= params.SstoreSentryGasEIP2200 { + return 0, errors.New("not enough gas for reentrancy sentry") + } + // Gas sentry honoured, do the actual gas calculation based on the stored value + var ( + y, x = stack.Back(1), stack.Back(0) + current = evm.StateDB.GetState(contract.Address(), x.Bytes32()) + ) + value := common.Hash(y.Bytes32()) + + if current == value { // noop (1) + return params.SloadGasEIP2200, nil + } + original := evm.StateDB.(withGetCommittedStateAP1).GetCommittedStateAP1(contract.Address(), x.Bytes32()) + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return params.SstoreSetGasEIP2200, nil + } + return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + } + + return params.SloadGasEIP2200, nil // dirty update (2.2) +} + +func gasCallExpertAP1(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + var ( + gas uint64 + transfersValue = !stack.Back(2).IsZero() + multiCoinTransfersValue = !stack.Back(4).IsZero() + address = common.Address(stack.Back(1).Bytes20()) + ) + if evm.chainRules.IsEIP158 { + if (transfersValue || multiCoinTransfersValue) && evm.StateDB.Empty(address) { + gas += params.CallNewAccountGas + } + } else if !evm.StateDB.Exist(address) { + gas += params.CallNewAccountGas + } + if transfersValue { + gas += params.CallValueTransferGas + } + if multiCoinTransfersValue { + gas += params.CallValueTransferGas + } + memoryGas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + var overflow bool + if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + return 0, ErrGasUintOverflow + } + + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) + if err != nil { + return 0, err + } + if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + +func gasSelfdestructAP1(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + var gas uint64 + // EIP150 homestead gas reprice fork: + if evm.chainRules.IsEIP150 { + gas = params.SelfdestructGasEIP150 + var address = common.Address(stack.Back(0).Bytes20()) + + if evm.chainRules.IsEIP158 { + // if empty and transfers value + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas += params.CreateBySelfdestructGas + } + } else if !evm.StateDB.Exist(address) { + gas += params.CreateBySelfdestructGas + } + } + + return gas, nil +} diff --git a/core/vm/instructions_ext.go b/core/vm/instructions_ext.go new file mode 100644 index 000000000000..6c429ff752b9 --- /dev/null +++ b/core/vm/instructions_ext.go @@ -0,0 +1,236 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "errors" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +type Multicoiner interface { + GetBalanceMultiCoin(stateDB StateDB, address common.Address, coinID common.Hash) *big.Int + CanTransferMC(stateDB StateDB, from common.Address, to common.Address, coinID common.Hash, amount *big.Int) bool + TransferMultiCoin(stateDB StateDB, from common.Address, to common.Address, coinID common.Hash, amount *big.Int) + UnpackNativeAssetCallInput(input []byte) (common.Address, common.Hash, *big.Int, []byte, error) +} + +func opBalanceMultiCoin(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + addr, cid := scope.Stack.pop(), scope.Stack.pop() + res, err := uint256.FromBig( + interpreter.evm.Config.Multicoiner.GetBalanceMultiCoin( + interpreter.evm.StateDB, + common.BigToAddress(addr.ToBig()), + common.BigToHash(cid.ToBig()), + ), + ) + if err { + return nil, errors.New("balance overflow") + } + scope.Stack.push(res) + return nil, nil +} + +func memoryCallExpert(stack *Stack) (uint64, bool) { + x, overflow := calcMemSize64(stack.Back(7), stack.Back(8)) + if overflow { + return 0, true + } + y, overflow := calcMemSize64(stack.Back(5), stack.Back(6)) + if overflow { + return 0, true + } + if x > y { + return x, false + } + return y, false +} + +// Note: opCallExpert was de-activated in ApricotPhase2. +func opCallExpert(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + stack := scope.Stack + // Pop gas. The actual gas in interpreter.evm.callGasTemp. + // We can use this as a temporary value + temp := stack.pop() + gas := interpreter.evm.callGasTemp + // Pop other call parameters. + addr, value, cid, value2, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() + toAddr := common.Address(addr.Bytes20()) + coinID := common.BigToHash(cid.ToBig()) + // Get the arguments from the memory. + args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) + + // Note: this code fails to check that value2 is zero, which was a bug when CALLEX was active. + // The CALLEX opcode was de-activated in ApricotPhase2 resolving this issue. + if interpreter.readOnly && !value.IsZero() { + return nil, ErrWriteProtection + } + var bigVal = big0 + //TODO: use uint256.Int instead of converting with toBig() + // By using big0 here, we save an alloc for the most common case (non-ether-transferring contract calls), + // but it would make more sense to extend the usage of uint256.Int + if !value.IsZero() { + gas += params.CallStipend + bigVal = value.ToBig() + } + + var bigVal2 = big0 + //TODO: use uint256.Int instead of converting with toBig() + // By using big0 here, we save an alloc for the most common case (non-ether-transferring contract calls), + // but it would make more sense to extend the usage of uint256.Int + if !value2.IsZero() { + bigVal2 = value2.ToBig() + } + + ret, returnGas, err := interpreter.evm.CallExpert(scope.Contract, toAddr, args, gas, bigVal, coinID, bigVal2) + if err != nil { + temp.Clear() + } else { + temp.SetOne() + } + stack.push(&temp) + if err == nil || err == ErrExecutionReverted { + scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) + } + scope.Contract.Gas += returnGas + + interpreter.returnData = ret + return ret, nil +} + +// This allows the user transfer balance of a specified coinId in addition to a normal Call(). +func (evm *EVM) CallExpert(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int, coinID common.Hash, value2 *big.Int) (ret []byte, leftOverGas uint64, err error) { + // Fail if we're trying to execute above the call depth limit + if evm.depth > int(params.CallCreateDepth) { + return nil, gas, ErrDepth + } + + // Fail if we're trying to transfer more than the available balance + // Note: it is not possible for a negative value to be passed in here due to the fact + // that [value] will be popped from the stack and decoded to a *big.Int, which will + // always yield a positive result. + if value.Sign() != 0 && !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { + return nil, gas, ErrInsufficientBalance + } + + if value2.Sign() != 0 && !evm.Config.Multicoiner.CanTransferMC(evm.StateDB, caller.Address(), addr, coinID, value2) { + return nil, gas, ErrInsufficientBalance + } + + snapshot := evm.StateDB.Snapshot() + //p, isPrecompile := evm.precompile(addr) + + if !evm.StateDB.Exist(addr) { + //if !isPrecompile && evm.chainRules.IsEIP158 && value.Sign() == 0 { + // // Calling a non existing account, don't do anything, but ping the tracer + // if evm.Config.Debug && evm.depth == 0 { + // evm.Config.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value) + // evm.Config.Tracer.CaptureEnd(ret, 0, 0, nil) + // } + // return nil, gas, nil + //} + evm.StateDB.CreateAccount(addr) + } + evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value) + evm.Config.Multicoiner.TransferMultiCoin(evm.StateDB, caller.Address(), addr, coinID, value2) + + // Capture the tracer start/end events in debug mode + debug := evm.Config.Tracer != nil + if debug && evm.depth == 0 { + evm.Config.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value) + defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters + evm.Config.Tracer.CaptureEnd(ret, startGas-gas, err) + }(gas, time.Now()) + } + + //if isPrecompile { + // ret, gas, err = RunPrecompiledContract(p, input, gas) + //} else { + // Initialise a new contract and set the code that is to be used by the EVM. + // The contract is a scoped environment for this execution context only. + code := evm.StateDB.GetCode(addr) + if len(code) == 0 { + ret, err = nil, nil // gas is unchanged + } else { + addrCopy := addr + // If the account has no code, we can abort here + // The depth-check is already done, and precompiles handled above + contract := NewContract(caller, AccountRef(addrCopy), value, gas) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code) + ret, err = evm.interpreter.Run(contract, input, false) + gas = contract.Gas + } + //} + // When an error was returned by the EVM or when setting the creation code + // above we revert to the snapshot and consume any gas remaining. Additionally + // when we're in homestead this also counts for code storage gas errors. + if err != nil { + evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { + gas = 0 + } + // TODO: consider clearing up unused snapshots: + //} else { + // evm.StateDB.DiscardSnapshot(snapshot) + } + return ret, gas, err +} + +func (evm *EVM) NativeAssetCall(caller common.Address, input []byte, suppliedGas uint64, gasCost uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if suppliedGas < gasCost { + return nil, 0, ErrOutOfGas + } + remainingGas = suppliedGas - gasCost + + if readOnly { + return nil, remainingGas, ErrExecutionReverted + } + + to, assetID, assetAmount, callData, err := evm.Config.Multicoiner.UnpackNativeAssetCallInput(input) + if err != nil { + return nil, remainingGas, ErrExecutionReverted + } + + // Note: it is not possible for a negative assetAmount to be passed in here due to the fact that decoding a + // byte slice into a *big.Int type will always return a positive value. + if assetAmount.Sign() != 0 && !evm.Config.Multicoiner.CanTransferMC(evm.StateDB, caller, to, assetID, assetAmount) { + return nil, remainingGas, ErrInsufficientBalance + } + + snapshot := evm.StateDB.Snapshot() + + if !evm.StateDB.Exist(to) { + if remainingGas < params.CallNewAccountGas { + return nil, 0, ErrOutOfGas + } + remainingGas -= params.CallNewAccountGas + evm.StateDB.CreateAccount(to) + } + + // Increment the call depth which is restricted to 1024 + evm.depth++ + defer func() { evm.depth-- }() + + // Send [assetAmount] of [assetID] to [to] address + evm.Config.Multicoiner.TransferMultiCoin(evm.StateDB, caller, to, assetID, assetAmount) + ret, remainingGas, err = evm.Call(AccountRef(caller), to, callData, remainingGas, new(big.Int)) + + // When an error was returned by the EVM or when setting the creation code + // above we revert to the snapshot and consume any gas remaining. Additionally + // when we're in homestead this also counts for code storage gas errors. + if err != nil { + evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { + remainingGas = 0 + } + // TODO: consider clearing up unused snapshots: + //} else { + // evm.StateDB.DiscardSnapshot(snapshot) + } + return ret, remainingGas, err +} diff --git a/core/vm/interface.go b/core/vm/interface.go index 26814d3d2f0e..859a4fea51d4 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -21,7 +21,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" ) // StateDB is an EVM database for full state querying. @@ -71,7 +70,6 @@ type StateDB interface { // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddSlotToAccessList(addr common.Address, slot common.Hash) - Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) RevertToSnapshot(int) Snapshot() int diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 28da2e80e60e..4d868048ac60 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -29,6 +29,14 @@ type Config struct { NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled + + ActivePrecompiles []common.Address + CustomPrecompiles map[common.Address]RunFunc + Multicoiner Multicoiner + JumpTable *JumpTable + IsProhibited func(addr common.Address) error + InterpreterHook func(contract *Contract) *Contract + CanDeploy func(addr common.Address) error } // ScopeContext contains the things that are per-call, such as stack and memory, @@ -56,6 +64,8 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter { // If jump table was not initialised we set the default one. var table *JumpTable switch { + case evm.Config.JumpTable != nil: + table = evm.Config.JumpTable case evm.chainRules.IsCancun: table = &cancunInstructionSet case evm.chainRules.IsShanghai: @@ -105,6 +115,10 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter { // considered a revert-and-consume-all-gas operation except for // ErrExecutionReverted which means revert-and-keep-gas-left. func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) { + if hook := in.evm.Config.InterpreterHook; hook != nil { + contract = hook(contract) + } + // Increment the call depth which is restricted to 1024 in.evm.depth++ defer func() { in.evm.depth-- }() diff --git a/core/vm/jump_table_ext.go b/core/vm/jump_table_ext.go new file mode 100644 index 000000000000..23cec7ad7b58 --- /dev/null +++ b/core/vm/jump_table_ext.go @@ -0,0 +1,139 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import "github.com/ethereum/go-ethereum/params" + +const ( + BALANCEMC = 0xcd + CALLEX = 0xcf +) + +var ( + LaunchInstructionSet = newLaunchInstructionSet() + ApricotPhase1InstructionSet = newApricotPhase1InstructionSet() + ApricotPhase2InstructionSet = newApricotPhase2InstructionSet() + ApricotPhase3InstructionSet = newApricotPhase3InstructionSet() + DurangoInstructionSet = newDurangoInstructionSet() + + SubnetEVMInstructionSet = newSubnetEVMInstructionSet() + SubnetEVMDurangoInstructionSet = newSubnetEVMDurangoInstructionSet() + SubnetEVMCancunInstructionSet = newSubnetEVMCancunInstructionSet() +) + +func newSubnetEVMCancunInstructionSet() JumpTable { + instructionSet := newSubnetEVMDurangoInstructionSet() + enable4844(&instructionSet) // EIP-4844 (BLOBHASH opcode) + enable7516(&instructionSet) // EIP-7516 (BLOBBASEFEE opcode) + enable1153(&instructionSet) // EIP-1153 "Transient Storage" + enable5656(&instructionSet) // EIP-5656 (MCOPY opcode) + enable6780(&instructionSet) // EIP-6780 SELFDESTRUCT only in same transaction + return validate(instructionSet) +} + +// newDurangoInstructionSet returns the frontier, homestead, byzantium, +// constantinople, istanbul, petersburg, subnet-evm, durango instructions. +func newSubnetEVMDurangoInstructionSet() JumpTable { + instructionSet := newSubnetEVMInstructionSet() + enable3855(&instructionSet) // PUSH0 instruction + enable3860(&instructionSet) // Limit and meter initcode + + return validate(instructionSet) +} + +// newSubnetEVMInstructionSet returns the frontier, homestead, byzantium, +// constantinople, istanbul, petersburg, subnet-evm instructions. +func newSubnetEVMInstructionSet() JumpTable { + instructionSet := newIstanbulInstructionSet() + enable2929(&instructionSet) + enable3198(&instructionSet) // Base fee opcode https://eips.ethereum.org/EIPS/eip-3198 + return validate(instructionSet) +} + +func newDurangoInstructionSet() JumpTable { + instructionSet := newApricotPhase3InstructionSet() + enable3855(&instructionSet) // PUSH0 instruction + enable3860(&instructionSet) // Limit and meter initcode + return validate(instructionSet) +} + +// newApricotPhase3InstructionSet returns the frontier, homestead, byzantium, +// constantinople, istanbul, petersburg, apricotPhase1, 2, and 3 instructions. +func newApricotPhase3InstructionSet() JumpTable { + instructionSet := newApricotPhase2InstructionSet() + enable3198(&instructionSet) // Base fee opcode https://eips.ethereum.org/EIPS/eip-3198 + return validate(instructionSet) +} + +// newApricotPhase1InstructionSet returns the frontier, +// homestead, byzantium, constantinople petersburg, +// istanbul, and apricotPhase1 instructions. +func newApricotPhase2InstructionSet() JumpTable { + instructionSet := newApricotPhase1InstructionSet() + + enable2929(&instructionSet) + enableAP2(&instructionSet) + + return validate(instructionSet) +} + +// newApricotPhase1InstructionSet returns the frontier, +// homestead, byzantium, constantinople petersburg, +// and istanbul instructions. +func newApricotPhase1InstructionSet() JumpTable { + instructionSet := newLaunchInstructionSet() + enableAP1(&instructionSet) + + return validate(instructionSet) +} + +func newLaunchInstructionSet() JumpTable { + instructionSet := newIstanbulInstructionSet() + enableFrontier_ext(&instructionSet) + enableEIP150_ext(&instructionSet) + + return validate(instructionSet) +} + +func enableEIP150_ext(jt *JumpTable) { + jt[CALLEX].constantGas = params.CallGasEIP150 +} + +func enableFrontier_ext(jt *JumpTable) { + opCodeToString[BALANCEMC] = "BALANCEMC" + opCodeToString[CALLEX] = "CALLEX" + stringToOp["BALANCEMC"] = BALANCEMC + stringToOp["CALLEX"] = CALLEX + + jt[BALANCEMC] = &operation{ + execute: opBalanceMultiCoin, + constantGas: params.BalanceGasFrontier, + minStack: minStack(2, 1), + maxStack: maxStack(2, 1), + } + jt[CALLEX] = &operation{ + execute: opCallExpert, + constantGas: params.CallGasFrontier, + dynamicGas: gasCall, + minStack: minStack(9, 1), + maxStack: maxStack(9, 1), + memorySize: memoryCallExpert, + } +} + +// enableAP1 disables gas refunds for SSTORE and SELFDESTRUCT. It is very +// similar to EIP-3298: Removal of Refunds [DRAFT] +// (https://eips.ethereum.org/EIPS/eip-3298). +func enableAP1(jt *JumpTable) { + jt[SSTORE].dynamicGas = gasSStoreAP1 + jt[SELFDESTRUCT].dynamicGas = gasSelfdestructAP1 + jt[CALLEX].dynamicGas = gasCallExpertAP1 +} + +func enableAP2(jt *JumpTable) { + jt[SSTORE].dynamicGas = _gasSStoreEIP2929 + jt[SELFDESTRUCT].dynamicGas = _gasSelfdestructEIP2929 + jt[BALANCEMC] = &operation{execute: opUndefined, maxStack: maxStack(0, 0)} + jt[CALLEX] = &operation{execute: opUndefined, maxStack: maxStack(0, 0)} +} diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 7c0028601dfb..0e803e5a8621 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -172,6 +172,8 @@ type StdTraceConfig struct { logger.Config Reexec *uint64 TxHash common.Hash + // Chain overrides, can be used to execute a trace using future fork rules + Overrides *params.ChainConfig `json:"overrides,omitempty"` } // txTraceResult is the result of a single transaction trace. diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go index 07c138bae47b..d31190392c50 100644 --- a/eth/tracers/js/goja.go +++ b/eth/tracers/js/goja.go @@ -279,8 +279,7 @@ func (t *jsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Addr t.ctx["value"] = valueBig t.ctx["block"] = t.vm.ToValue(env.Context.BlockNumber.Uint64()) // Update list of precompiles based on current block - rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil, env.Context.Time) - t.activePrecompiles = vm.ActivePrecompiles(rules) + t.activePrecompiles = env.ActivePrecompiles() } // CaptureState implements the Tracer interface to trace a single step of VM execution. diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 2b36f9f4922f..d9e2c046c0bc 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -30,7 +30,6 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -54,8 +53,6 @@ type Config struct { EnableReturnData bool // enable return data capture Debug bool // print output during capture end Limit int // maximum length of output, but zero means unlimited - // Chain overrides, can be used to execute a trace using future fork rules - Overrides *params.ChainConfig `json:"overrides,omitempty"` } //go:generate go run github.com/fjl/gencodec -type StructLog -field-override structLogMarshaling -out gen_structlog.go diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index 5a2c4f91115f..e2ee63b4d831 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -81,8 +81,7 @@ func (t *fourByteTracer) store(id []byte, size int) { // CaptureStart implements the EVMLogger interface to initialize the tracing operation. func (t *fourByteTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { // Update list of precompiles based on current block - rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil, env.Context.Time) - t.activePrecompiles = vm.ActivePrecompiles(rules) + t.activePrecompiles = env.ActivePrecompiles() // Save the outer calldata also if len(input) >= 4 { diff --git a/eth/tracers/native/call_flat.go b/eth/tracers/native/call_flat.go index 266ab9900146..00f2b499c6f2 100644 --- a/eth/tracers/native/call_flat.go +++ b/eth/tracers/native/call_flat.go @@ -147,8 +147,7 @@ func newFlatCallTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Trace func (t *flatCallTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { t.tracer.CaptureStart(env, from, to, create, input, gas, value) // Update list of precompiles based on current block - rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil, env.Context.Time) - t.activePrecompiles = vm.ActivePrecompiles(rules) + t.activePrecompiles = env.ActivePrecompiles() } // CaptureEnd is called after the call finishes to finalize the tracing. diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index 82451c40a65f..795e03de45f4 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -290,6 +290,14 @@ 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) { + // 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/trie/committer.go b/trie/committer.go index 92163cdb3b64..b5e9476e2f04 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -156,6 +156,7 @@ func (c *committer) store(path []byte, n node) node { // mptResolver the children resolver in merkle-patricia-tree. type mptResolver struct{} +type MptResolver = mptResolver // ForEach implements childResolver, decodes the provided node and // traverses the children inside. diff --git a/trie/database.go b/trie/database.go index e20f7ef903b0..0f3d2373b190 100644 --- a/trie/database.go +++ b/trie/database.go @@ -22,18 +22,18 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie/triedb/database" "github.com/ethereum/go-ethereum/trie/triedb/hashdb" - "github.com/ethereum/go-ethereum/trie/triedb/pathdb" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" ) // 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 database.HashBackendFactory // Configs for hash-based scheme + PathDB database.PathBackendFactory // Configs for experimental path-based scheme } // HashDefaults represents a config for using hash-based scheme with @@ -43,47 +43,14 @@ var HashDefaults = &Config{ HashDB: hashdb.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 - - // 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 -} - // Database is the wrapper of the underlying backend which is shared by different // types of node backend as an entrypoint. It's responsible for all interactions // relevant with trie nodes and node preimages. type Database struct { - config *Config // Configuration for trie database - diskdb ethdb.Database // Persistent database to store the snapshot - preimages *preimageStore // The store for caching preimages - backend backend // The backend for managing trie nodes + config *Config // Configuration for trie database + diskdb ethdb.Database // Persistent database to store the snapshot + preimages *preimageStore // The store for caching preimages + backend database.Backend // The backend for managing trie nodes } // NewDatabase initializes the trie database with default settings, note @@ -106,23 +73,19 @@ 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 if config.HashDB != nil { + db.backend = config.HashDB.New(diskdb, mptResolver{}) } else { - db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) + db.backend = HashDefaults.HashDB.New(diskdb, mptResolver{}) } return db } // 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) (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") +func (db *Database) Reader(blockRoot common.Hash) (database.Reader, error) { + return db.backend.Reader(blockRoot) } // Update performs a state transition by committing dirty nodes contained in the @@ -136,6 +99,15 @@ func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, n if db.preimages != nil { db.preimages.commit(false) } + if root == parent { + type withUpdateSameRoot interface { + UpdateSameRoot(common.Hash) error + } + if u, ok := db.backend.(withUpdateSameRoot); ok { + return u.UpdateSameRoot(root) + } + return nil + } return db.backend.Update(root, parent, block, nodes, states) } @@ -205,7 +177,7 @@ func (db *Database) Preimage(hash 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") } @@ -221,7 +193,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") } @@ -232,7 +204,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") } @@ -245,7 +217,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") } @@ -256,7 +228,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") } @@ -269,7 +241,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") } @@ -279,7 +251,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") } @@ -291,7 +263,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") } @@ -302,7 +274,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/trie/triedb/database/database.go b/trie/triedb/database/database.go new file mode 100644 index 000000000000..0424c866b666 --- /dev/null +++ b/trie/triedb/database/database.go @@ -0,0 +1,123 @@ +// Copyright 2024 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 database + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/ethereum/go-ethereum/trie/triestate" +) + +// Reader wraps the Node method of a backing trie reader. +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. + // + // 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) +} + +// PreimageStore wraps the methods of a backing store for reading and writing +// trie node preimages. +type PreimageStore interface { + // Preimage retrieves the preimage of the specified hash. + Preimage(hash common.Hash) []byte + + // InsertPreimage commits a set of preimages along with their hashes. + InsertPreimage(preimages map[common.Hash][]byte) +} + +// Database wraps the methods of a backing trie store. +type Database interface { + PreimageStore + + // 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) +} + +// 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) +} + +// ChildResolver defines the required method to decode the provided +// trie node and iterate the children on top. +type ChildResolver interface { + ForEach(node []byte, onChild func(common.Hash)) +} + +type HashBackendFactory interface { + New(diskdb ethdb.Database, resolver ChildResolver) HashBackend +} + +type PathBackend interface { + Backend + Recover(target 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 +} + +type PathBackendFactory interface { + New(diskdb ethdb.Database) PathBackend +} diff --git a/trie/triedb/hashdb/database.go b/trie/triedb/hashdb/database.go index e45ccdba32ca..b1708b7a8b8b 100644 --- a/trie/triedb/hashdb/database.go +++ b/trie/triedb/hashdb/database.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/triedb/database" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" ) @@ -59,12 +60,6 @@ var ( memcacheCommitBytesMeter = metrics.NewRegisteredMeter("hashdb/memcache/commit/bytes", nil) ) -// ChildResolver defines the required method to decode the provided -// trie node and iterate the children on top. -type ChildResolver interface { - ForEach(node []byte, onChild func(common.Hash)) -} - // Config contains the settings for database. type Config struct { CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes @@ -79,12 +74,16 @@ var Defaults = &Config{ CleanCacheSize: 0, } +func (c *Config) New(diskdb ethdb.Database, resolver database.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. type Database struct { - diskdb ethdb.Database // Persistent storage for matured trie nodes - resolver ChildResolver // The handler to resolve children of nodes + diskdb ethdb.Database // Persistent storage for matured trie nodes + resolver database.ChildResolver // The handler to resolve children of nodes cleans *fastcache.Cache // GC friendly memory cache of clean node RLPs dirties map[common.Hash]*cachedNode // Data and references relationships of dirty trie nodes @@ -123,7 +122,7 @@ var cachedNodeSize = int(reflect.TypeOf(cachedNode{}).Size()) // forChildren invokes the callback for all the tracked children of this node, // both the implicit ones from inside the node as well as the explicit ones // from outside the node. -func (n *cachedNode) forChildren(resolver ChildResolver, onChild func(hash common.Hash)) { +func (n *cachedNode) forChildren(resolver database.ChildResolver, onChild func(hash common.Hash)) { for child := range n.external { onChild(child) } @@ -131,7 +130,7 @@ func (n *cachedNode) forChildren(resolver ChildResolver, onChild func(hash commo } // New initializes the hash-based node database. -func New(diskdb ethdb.Database, config *Config, resolver ChildResolver) *Database { +func New(diskdb ethdb.Database, config *Config, resolver database.ChildResolver) *Database { if config == nil { config = Defaults } @@ -631,7 +630,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/trie/triedb/pathdb/database.go b/trie/triedb/pathdb/database.go index f2d6cea635a9..1cfef3418a23 100644 --- a/trie/triedb/pathdb/database.go +++ b/trie/triedb/pathdb/database.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie/triedb/database" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" ) @@ -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)