diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index b65bdf96039..e000c563e41 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -27,6 +27,7 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/rlp" @@ -348,7 +349,9 @@ func (t *Tree) Snapshots(root common.Hash, limits int, nodisk bool) []Snapshot { // 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 { +// +// libevm: Options are ignored and only included to match an interface method. +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, _ ...stateconf.SnapshotUpdateOption) error { // Reject noop updates to avoid self-loops in the snapshot tree. This is a // special case that can only happen for Clique networks where empty blocks // don't modify the state (0 block subsidy). diff --git a/core/state/statedb.go b/core/state/statedb.go index 3b706002e76..ffdfe54299b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -27,6 +27,7 @@ import ( "github.com/ava-labs/libevm/core/state/snapshot" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/params" @@ -63,7 +64,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 +142,8 @@ 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) { + snaps = clearTypedNilPointer(snaps) tr, err := db.OpenTrie(root) if err != nil { return nil, err @@ -1162,7 +1164,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, opts ...stateconf.SnapshotUpdateOption) (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) @@ -1252,7 +1254,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er 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 { + if err := s.snaps.Update(root, parent, s.convertAccountSet(s.stateObjectsDestruct), s.accounts, s.storages, opts...); 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. diff --git a/core/state/statedb.libevm.go b/core/state/statedb.libevm.go new file mode 100644 index 00000000000..7db3d676e97 --- /dev/null +++ b/core/state/statedb.libevm.go @@ -0,0 +1,54 @@ +// 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 ( + "reflect" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/state/snapshot" + "github.com/ava-labs/libevm/libevm/stateconf" +) + +// SnapshotTree mirrors the functionality of a [snapshot.Tree], allowing for +// drop-in replacements. This is intended as a temporary feature as a workaround +// until a standard Tree can be used. +type SnapshotTree interface { + Cap(common.Hash, int) error + Snapshot(common.Hash) snapshot.Snapshot + StorageIterator(root, account, seek common.Hash) (snapshot.StorageIterator, error) + 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, + opts ...stateconf.SnapshotUpdateOption, + ) error +} + +var _ SnapshotTree = (*snapshot.Tree)(nil) + +// clearTypedNilPointer returns nil if `snaps == nil` or if it holds a nil +// pointer. The default geth behaviour expected a [snapshot.Tree] pointer +// instead of a SnapshotTree interface, which could result in typed-nil bugs. +func clearTypedNilPointer(snaps SnapshotTree) SnapshotTree { + if v := reflect.ValueOf(snaps); v.Kind() == reflect.Pointer && v.IsNil() { + return nil + } + return snaps +} diff --git a/core/state/statedb.libevm_test.go b/core/state/statedb.libevm_test.go new file mode 100644 index 00000000000..98eff41e5c8 --- /dev/null +++ b/core/state/statedb.libevm_test.go @@ -0,0 +1,80 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state/snapshot" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/libevm/stateconf" +) + +func TestStateDBCommitPropagatesOptions(t *testing.T) { + var rec snapTreeRecorder + sdb, err := New(types.EmptyRootHash, NewDatabase(rawdb.NewMemoryDatabase()), &rec) + require.NoError(t, err, "New()") + + // Ensures that rec.Update() will be called. + sdb.SetNonce(common.Address{}, 42) + + const payload = "hello world" + opt := stateconf.WithUpdatePayload(payload) + _, err = sdb.Commit(0, false, opt) + require.NoErrorf(t, err, "%T.Commit(..., %T)", sdb, opt) + + assert.Equalf(t, payload, rec.gotPayload, "%T payload propagated via %T.Commit() to %T.Update()", opt, sdb, rec) +} + +type snapTreeRecorder struct { + SnapshotTree + gotPayload any +} + +func (*snapTreeRecorder) Cap(common.Hash, int) error { + return nil +} + +func (r *snapTreeRecorder) Update( + _, _ common.Hash, + _ map[common.Hash]struct{}, _ map[common.Hash][]byte, _ map[common.Hash]map[common.Hash][]byte, + opts ...stateconf.SnapshotUpdateOption, +) error { + r.gotPayload = stateconf.ExtractUpdatePayload(opts...) + return nil +} + +func (*snapTreeRecorder) Snapshot(common.Hash) snapshot.Snapshot { + return snapshotStub{} +} + +type snapshotStub struct { + snapshot.Snapshot +} + +func (snapshotStub) Account(common.Hash) (*types.SlimAccount, error) { + return &types.SlimAccount{}, nil +} + +func (snapshotStub) Root() common.Hash { + return common.Hash{} +} diff --git a/libevm/stateconf/conf.go b/libevm/stateconf/conf.go new file mode 100644 index 00000000000..5b7971f2034 --- /dev/null +++ b/libevm/stateconf/conf.go @@ -0,0 +1,45 @@ +// 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 stateconf configures state management. +package stateconf + +import "github.com/ava-labs/libevm/libevm/options" + +// A SnapshotUpdateOption configures the behaviour of +// state.SnapshotTree.Update() implementations. This will be removed along with +// state.SnapshotTree. +type SnapshotUpdateOption = options.Option[snapshotUpdateConfig] + +type snapshotUpdateConfig struct { + payload any +} + +// WithUpdatePayload returns a SnapshotUpdateOption carrying an arbitrary +// payload. It acts only as a carrier to exploit existing function plumbing and +// the effect on behaviour is left to the implementation receiving it. +func WithUpdatePayload(p any) SnapshotUpdateOption { + return options.Func[snapshotUpdateConfig](func(c *snapshotUpdateConfig) { + c.payload = p + }) +} + +// ExtractUpdatePayload returns the payload carried by a [WithUpdatePayload] +// option. Only one such option can be used at once; behaviour is otherwise +// undefined. +func ExtractUpdatePayload(opts ...SnapshotUpdateOption) any { + return options.As(opts...).payload +}