diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go
index 04b10d295cf..3a2957edd14 100644
--- a/core/types/rlp_payload.libevm.go
+++ b/core/types/rlp_payload.libevm.go
@@ -21,6 +21,7 @@ import (
"io"
"github.com/ethereum/go-ethereum/libevm/pseudo"
+ "github.com/ethereum/go-ethereum/libevm/testonly"
"github.com/ethereum/go-ethereum/rlp"
)
@@ -51,6 +52,18 @@ func RegisterExtras[SA any]() ExtraPayloads[SA] {
return extra
}
+// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
+// [RegisterExtras]. It panics if called from a non-testing call stack.
+//
+// In tests it SHOULD be called before every call to [RegisterExtras] and then
+// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is
+// a workaround for the single-call limitation on [RegisterExtras].
+func TestOnlyClearRegisteredExtras() {
+ testonly.OrPanic(func() {
+ registeredExtras = nil
+ })
+}
+
var registeredExtras *extraConstructors
type extraConstructors struct {
@@ -126,6 +139,27 @@ func (e *StateAccountExtra) payload() *pseudo.Type {
return e.t
}
+// Equal reports whether `e` is semantically equivalent to `f` for the purpose
+// of tests.
+//
+// Equal MUST NOT be used in production. Instead, compare values returned by
+// [ExtraPayloads.FromStateAccount].
+func (e *StateAccountExtra) Equal(f *StateAccountExtra) bool {
+ if false {
+ // TODO(arr4n): calling this results in an error from cmp.Diff():
+ // "non-deterministic or non-symmetric function detected". Explore the
+ // issue and then enable the enforcement.
+ testonly.OrPanic(func() {})
+ }
+
+ eNil := e == nil || e.t == nil
+ fNil := f == nil || f.t == nil
+ if eNil && fNil || eNil && f.t.IsZero() || fNil && e.t.IsZero() {
+ return true
+ }
+ return e.t.Equal(f.t)
+}
+
var _ interface {
rlp.Encoder
rlp.Decoder
diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go
index fa5810ed84e..c7fe6d50621 100644
--- a/core/types/state_account.libevm_test.go
+++ b/core/types/state_account.libevm_test.go
@@ -30,15 +30,6 @@ import (
"github.com/ethereum/go-ethereum/rlp"
)
-func (e *StateAccountExtra) Equal(f *StateAccountExtra) bool {
- eNil := e == nil || e.t == nil
- fNil := f == nil || f.t == nil
- if eNil && fNil || eNil && f.t.IsZero() || fNil && e.t.IsZero() {
- return true
- }
- return e.t.Equal(f.t)
-}
-
func TestStateAccountRLP(t *testing.T) {
// RLP encodings that don't involve extra payloads were generated on raw
// geth StateAccounts *before* any libevm modifications, thus locking in
@@ -123,11 +114,9 @@ func TestStateAccountRLP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.register != nil {
- registeredExtras = nil
+ TestOnlyClearRegisteredExtras()
tt.register()
- t.Cleanup(func() {
- registeredExtras = nil
- })
+ t.Cleanup(TestOnlyClearRegisteredExtras)
}
assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex)
diff --git a/core/types/state_account_storage.libevm_test.go b/core/types/state_account_storage.libevm_test.go
new file mode 100644
index 00000000000..e32f065293c
--- /dev/null
+++ b/core/types/state_account_storage.libevm_test.go
@@ -0,0 +1,153 @@
+// 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 types_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/holiman/uint256"
+ "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/types"
+ "github.com/ethereum/go-ethereum/libevm/ethtest"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/triedb"
+)
+
+func TestStateAccountExtraViaTrieStorage(t *testing.T) {
+ rng := ethtest.NewPseudoRand(1984)
+ addr := rng.Address()
+
+ type arbitraryPayload struct {
+ Data string
+ }
+ const arbitraryData = "Hello, RLP world!"
+
+ var (
+ // The specific trie hashes after inserting the account are irrelevant;
+ // what's important is that: (a) they are all different; and (b) tests
+ // of implicit and explicit zero-value payloads have the same hash.
+ vanillaGeth = common.HexToHash("0x2108846aaec8a88cfa02887527ad8c1beffc11b5ec428b68f15d9ce4e71e4ce1")
+ trueBool = common.HexToHash("0x665576885e52711e4cf90b72750fc1c17c80c5528bc54244e327414d486a10a4")
+ falseBool = common.HexToHash("0xa53fcb27d01347e202fb092d0af2a809cb84390c6001cbc151052ee29edc2294")
+ arbitrary = common.HexToHash("0x94eecff1444ab69437636630918c15596e001b30b973f03e06006ae20aa6e307")
+ )
+
+ tests := []struct {
+ name string
+ registerAndSetExtra func(*types.StateAccount) *types.StateAccount
+ assertExtra func(*testing.T, *types.StateAccount)
+ wantTrieHash common.Hash
+ }{
+ {
+ name: "vanilla geth",
+ registerAndSetExtra: func(a *types.StateAccount) *types.StateAccount {
+ return a
+ },
+ assertExtra: func(t *testing.T, a *types.StateAccount) {
+ t.Helper()
+ assert.Truef(t, a.Extra.Equal(nil), "%T.%T.IsEmpty()", a, a.Extra)
+ },
+ wantTrieHash: vanillaGeth,
+ },
+ {
+ name: "true-boolean payload",
+ registerAndSetExtra: func(a *types.StateAccount) *types.StateAccount {
+ types.RegisterExtras[bool]().SetOnStateAccount(a, true)
+ return a
+ },
+ assertExtra: func(t *testing.T, sa *types.StateAccount) {
+ t.Helper()
+ assert.Truef(t, types.ExtraPayloads[bool]{}.FromStateAccount(sa), "")
+ },
+ wantTrieHash: trueBool,
+ },
+ {
+ name: "explicit false-boolean payload",
+ registerAndSetExtra: func(a *types.StateAccount) *types.StateAccount {
+ p := types.RegisterExtras[bool]()
+ p.SetOnStateAccount(a, false) // the explicit part
+ return a
+ },
+ assertExtra: func(t *testing.T, sa *types.StateAccount) {
+ t.Helper()
+ assert.Falsef(t, types.ExtraPayloads[bool]{}.FromStateAccount(sa), "")
+ },
+ wantTrieHash: falseBool,
+ },
+ {
+ name: "implicit false-boolean payload",
+ registerAndSetExtra: func(a *types.StateAccount) *types.StateAccount {
+ types.RegisterExtras[bool]()
+ // Note that `a` is reflected, unchanged (the implicit part).
+ return a
+ },
+ assertExtra: func(t *testing.T, sa *types.StateAccount) {
+ t.Helper()
+ assert.Falsef(t, types.ExtraPayloads[bool]{}.FromStateAccount(sa), "")
+ },
+ wantTrieHash: falseBool,
+ },
+ {
+ name: "arbitrary payload",
+ registerAndSetExtra: func(a *types.StateAccount) *types.StateAccount {
+ p := arbitraryPayload{arbitraryData}
+ types.RegisterExtras[arbitraryPayload]().SetOnStateAccount(a, p)
+ return a
+ },
+ assertExtra: func(t *testing.T, sa *types.StateAccount) {
+ t.Helper()
+ got := types.ExtraPayloads[arbitraryPayload]{}.FromStateAccount(sa)
+ assert.Equalf(t, arbitraryPayload{arbitraryData}, got, "")
+ },
+ wantTrieHash: arbitrary,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ types.TestOnlyClearRegisteredExtras()
+ t.Cleanup(types.TestOnlyClearRegisteredExtras)
+
+ acct := tt.registerAndSetExtra(&types.StateAccount{
+ Nonce: 42,
+ Balance: uint256.NewInt(314159),
+ Root: types.EmptyRootHash,
+ CodeHash: types.EmptyCodeHash[:],
+ })
+
+ db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
+ id := trie.TrieID(types.EmptyRootHash)
+ state, err := trie.NewStateTrie(id, db)
+ require.NoError(t, err, "trie.NewStateTrie(types.EmptyRootHash, ...)")
+
+ require.NoErrorf(t, state.UpdateAccount(addr, acct), "%T.UpdateAccount(...)", state)
+ assert.Equalf(t, tt.wantTrieHash, state.Hash(), "%T.Hash() after UpdateAccount()", state)
+
+ got, err := state.GetAccount(addr)
+ require.NoError(t, err, "state.GetAccount({account updated earlier})")
+ if diff := cmp.Diff(acct, got); diff != "" {
+ t.Errorf("%T.GetAccount() not equal to value passed to %[1]T.UpdateAccount(); diff (-want +got):\n%s", state, diff)
+ }
+ tt.assertExtra(t, got)
+ })
+ }
+}
diff --git a/libevm/testonly/testonly.go b/libevm/testonly/testonly.go
new file mode 100644
index 00000000000..74a9a81d6f3
--- /dev/null
+++ b/libevm/testonly/testonly.go
@@ -0,0 +1,40 @@
+// 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 testonly enforces functionality that MUST be limited to tests.
+package testonly
+
+import (
+ "runtime"
+ "strings"
+)
+
+// OrPanic runs `fn` i.f.f. called from within a testing environment.
+func OrPanic(fn func()) {
+ pc := make([]uintptr, 64)
+ runtime.Callers(0, pc)
+ frames := runtime.CallersFrames(pc)
+ for {
+ f, more := frames.Next()
+ if strings.Contains(f.File, "/testing/") || strings.HasSuffix(f.File, "_test.go") {
+ fn()
+ return
+ }
+ if !more {
+ panic("no _test.go file in call stack")
+ }
+ }
+}
diff --git a/params/config.libevm.go b/params/config.libevm.go
index 0e9981bcc32..c24f47537fb 100644
--- a/params/config.libevm.go
+++ b/params/config.libevm.go
@@ -19,10 +19,9 @@ import (
"fmt"
"math/big"
"reflect"
- "runtime"
- "strings"
"github.com/ethereum/go-ethereum/libevm/pseudo"
+ "github.com/ethereum/go-ethereum/libevm/testonly"
)
// Extras are arbitrary payloads to be added as extra fields in [ChainConfig]
@@ -92,19 +91,9 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is
// a workaround for the single-call limitation on [RegisterExtras].
func TestOnlyClearRegisteredExtras() {
- pc := make([]uintptr, 10)
- runtime.Callers(0, pc)
- frames := runtime.CallersFrames(pc)
- for {
- f, more := frames.Next()
- if strings.Contains(f.File, "/testing/") || strings.HasSuffix(f.File, "_test.go") {
- registeredExtras = nil
- return
- }
- if !more {
- panic("no _test.go file in call stack")
- }
- }
+ testonly.OrPanic(func() {
+ registeredExtras = nil
+ })
}
// registeredExtras holds non-generic constructors for the [Extras] types