From 01bf6c2d1f95f5b1bebdefad1a5db170b20c1979 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 4 Oct 2024 10:14:36 +0100 Subject: [PATCH] feat: `state.{Get,Set}Extra[SA any](*StateDB,types.ExtraPayloads,...)` --- core/state/state.libevm.go | 64 ++++++++++++++++ core/state/state.libevm_test.go | 130 ++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 core/state/state.libevm.go create mode 100644 core/state/state.libevm_test.go diff --git a/core/state/state.libevm.go b/core/state/state.libevm.go new file mode 100644 index 000000000000..c08a6a9b5fa4 --- /dev/null +++ b/core/state/state.libevm.go @@ -0,0 +1,64 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package state + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// GetExtra returns the extra payload from the [types.StateAccount] associated +// with the address, or a zero-value `SA` if not found. The +// [types.ExtraPayloads] MUST be sourced from [types.RegisterExtras]. +func GetExtra[SA any](s *StateDB, p types.ExtraPayloads[SA], addr common.Address) SA { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return p.FromStateAccount(&stateObject.data) + } + var zero SA + return zero +} + +// SetExtra sets the extra payload for the address. See [GetExtra] for details. +func SetExtra[SA any](s *StateDB, p types.ExtraPayloads[SA], addr common.Address, extra SA) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + setExtraOnObject(stateObject, p, addr, extra) + } +} + +func setExtraOnObject[SA any](s *stateObject, p types.ExtraPayloads[SA], addr common.Address, extra SA) { + s.db.journal.append(extraChange[SA]{ + payloads: p, + account: &addr, + prev: p.FromStateAccount(&s.data), + }) + p.SetOnStateAccount(&s.data, extra) +} + +// extraChange is a [journalEntry] for [SetExtra] / [setExtraOnObject]. +type extraChange[SA any] struct { + payloads types.ExtraPayloads[SA] + account *common.Address + prev SA +} + +func (e extraChange[SA]) dirtied() *common.Address { return e.account } + +func (e extraChange[SA]) revert(s *StateDB) { + e.payloads.SetOnStateAccount(&s.getStateObject(*e.account).data, e.prev) +} diff --git a/core/state/state.libevm_test.go b/core/state/state.libevm_test.go new file mode 100644 index 000000000000..274a31537fd9 --- /dev/null +++ b/core/state/state.libevm_test.go @@ -0,0 +1,130 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package state_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/libevm/ethtest" + "github.com/ethereum/go-ethereum/triedb" +) + +func TestGetSetExtra(t *testing.T) { + types.TestOnlyClearRegisteredExtras() + t.Cleanup(types.TestOnlyClearRegisteredExtras) + payloads := types.RegisterExtras[[]byte]() + + rng := ethtest.NewPseudoRand(42) + addr := rng.Address() + nonce := rng.Uint64() + balance := rng.Uint256() + extra := rng.Bytes(8) + + views := newWithSnaps(t) + stateDB := views.stateDB + assert.Nilf(t, state.GetExtra(stateDB, payloads, addr), "state.GetExtra() returns zero-value %T if before SetExtra()", extra) + stateDB.CreateAccount(addr) + stateDB.SetNonce(addr, nonce) + stateDB.SetBalance(addr, balance) + state.SetExtra(stateDB, payloads, addr, extra) + + root, err := stateDB.Commit(1, false) // arbitrary block number + require.NoErrorf(t, err, "%T.Commit(1, false)", stateDB) + require.NotEqualf(t, types.EmptyRootHash, root, "root hash returned by %T.Commit() is not the empty root", stateDB) + + t.Run(fmt.Sprintf("retrieve from %T", views.snaps), func(t *testing.T) { + iter, err := views.snaps.AccountIterator(root, common.Hash{}) + require.NoErrorf(t, err, "%T.AccountIterator(...)", views.snaps) + defer iter.Release() + + require.Truef(t, iter.Next(), "%T.Next() (i.e. at least one account)", iter) + require.NoErrorf(t, iter.Error(), "%T.Error()", iter) + + t.Run("types.FullAccount()", func(t *testing.T) { + got, err := types.FullAccount(iter.Account()) + require.NoErrorf(t, err, "types.FullAccount(%T.Account())", iter) + + want := &types.StateAccount{ + Nonce: nonce, + Balance: balance, + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + } + payloads.SetOnStateAccount(want, extra) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("types.FullAccount(%T.Account()) diff (-want +got):\n%s", iter, diff) + } + }) + + require.Falsef(t, iter.Next(), "%T.Next() after first account (i.e. only one)", iter) + }) + + t.Run(fmt.Sprintf("retrieve from new %T", views.stateDB), func(t *testing.T) { + stateDB, err := state.New(root, views.database, views.snaps) + require.NoError(t, err, "state.New()") + + // triggers SlimAccount RLP decoding + assert.Equalf(t, nonce, stateDB.GetNonce(addr), "%T.GetNonce()", stateDB) + assert.Equalf(t, balance, stateDB.GetBalance(addr), "%T.GetBalance()", stateDB) + assert.Equal(t, extra, state.GetExtra(stateDB, payloads, addr), "state.GetExtra()") + }) +} + +// stateViews are different ways to access the same data. +type stateViews struct { + stateDB *state.StateDB + snaps *snapshot.Tree + database state.Database +} + +func newWithSnaps(t *testing.T) stateViews { + t.Helper() + empty := types.EmptyRootHash + kvStore := memorydb.New() + ethDB := rawdb.NewDatabase(kvStore) + snaps, err := snapshot.New( + snapshot.Config{ + CacheSize: 16, // Mb (arbitrary but non-zero) + }, + kvStore, + triedb.NewDatabase(ethDB, nil), + empty, + ) + require.NoError(t, err, "snapshot.New()") + + database := state.NewDatabase(ethDB) + stateDB, err := state.New(empty, database, snaps) + require.NoError(t, err, "state.New()") + + return stateViews{ + stateDB: stateDB, + snaps: snaps, + database: database, + } +}