Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion core/state/snapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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).
Expand Down
10 changes: 6 additions & 4 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions core/state/statedb.libevm.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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
}
80 changes: 80 additions & 0 deletions core/state/statedb.libevm_test.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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{}
}
45 changes: 45 additions & 0 deletions libevm/stateconf/conf.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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