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
+}