diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 1282b281d637..ec48e30d725d 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -18,6 +18,6 @@ jobs:
go-version: 1.21.4
- name: Run tests
run: | # Upstream flakes are race conditions exacerbated by concurrent tests
- FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
+ FLAKY_REGEX='go-ethereum/(eth|eth/tracers/js|eth/tracers/logger|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
go list ./... | grep -P "${FLAKY_REGEX}" | xargs -n 1 go test -short;
go test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}");
diff --git a/core/state/statedb.go b/core/state/statedb.go
index a4b8cf93e2d2..8092155ce595 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -579,6 +579,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
Balance: acc.Balance,
CodeHash: acc.CodeHash,
Root: common.BytesToHash(acc.Root),
+ Extra: acc.Extra, // no need to deep-copy as `acc` is short-lived
}
if len(data.CodeHash) == 0 {
data.CodeHash = types.EmptyCodeHash.Bytes()
diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go
index 8b424493afb8..9205318512eb 100644
--- a/core/types/gen_account_rlp.go
+++ b/core/types/gen_account_rlp.go
@@ -16,6 +16,9 @@ func (obj *StateAccount) EncodeRLP(_w io.Writer) error {
}
w.WriteBytes(obj.Root[:])
w.WriteBytes(obj.CodeHash)
+ if err := obj.Extra.EncodeRLP(w); err != nil {
+ return err
+ }
w.ListEnd(_tmp0)
return w.Flush()
}
diff --git a/core/types/gen_slim_account_rlp.libevm.go b/core/types/gen_slim_account_rlp.libevm.go
new file mode 100644
index 000000000000..e5d76069a750
--- /dev/null
+++ b/core/types/gen_slim_account_rlp.libevm.go
@@ -0,0 +1,24 @@
+// Code generated by rlpgen. DO NOT EDIT.
+
+package types
+
+import "github.com/ethereum/go-ethereum/rlp"
+import "io"
+
+func (obj *SlimAccount) EncodeRLP(_w io.Writer) error {
+ w := rlp.NewEncoderBuffer(_w)
+ _tmp0 := w.List()
+ w.WriteUint64(obj.Nonce)
+ if obj.Balance == nil {
+ w.Write(rlp.EmptyString)
+ } else {
+ w.WriteUint256(obj.Balance)
+ }
+ w.WriteBytes(obj.Root)
+ w.WriteBytes(obj.CodeHash)
+ if err := obj.Extra.EncodeRLP(w); err != nil {
+ return err
+ }
+ w.ListEnd(_tmp0)
+ return w.Flush()
+}
diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go
new file mode 100644
index 000000000000..3a2957edd14f
--- /dev/null
+++ b/core/types/rlp_payload.libevm.go
@@ -0,0 +1,209 @@
+// 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
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/ethereum/go-ethereum/libevm/pseudo"
+ "github.com/ethereum/go-ethereum/libevm/testonly"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// RegisterExtras registers the type `SA` to be carried as an extra payload in
+// [StateAccount] structs. It is expected to be called in an `init()` function
+// and MUST NOT be called more than once.
+//
+// The payload will be treated as an extra struct field for the purposes of RLP
+// encoding and decoding. RLP handling is plumbed through to the `SA` via the
+// [StateAccountExtra] that holds it such that it acts as if there were a field
+// of type `SA` in all StateAccount structs.
+//
+// The payload can be acced via the [ExtraPayloads.FromStateAccount] method of
+// the accessor returned by RegisterExtras.
+func RegisterExtras[SA any]() ExtraPayloads[SA] {
+ if registeredExtras != nil {
+ panic("re-registration of Extras")
+ }
+ var extra ExtraPayloads[SA]
+ registeredExtras = &extraConstructors{
+ stateAccountType: func() string {
+ var x SA
+ return fmt.Sprintf("%T", x)
+ }(),
+ newStateAccount: pseudo.NewConstructor[SA]().Zero,
+ cloneStateAccount: extra.cloneStateAccount,
+ }
+ 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 {
+ stateAccountType string
+ newStateAccount func() *pseudo.Type
+ cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
+}
+
+func (e *StateAccountExtra) clone() *StateAccountExtra {
+ switch r := registeredExtras; {
+ case r == nil, e == nil:
+ return nil
+ default:
+ return r.cloneStateAccount(e)
+ }
+}
+
+// ExtraPayloads provides strongly typed access to the extra payload carried by
+// [StateAccount] structs. The only valid way to construct an instance is by a
+// call to [RegisterExtras].
+type ExtraPayloads[SA any] struct {
+ _ struct{} // make godoc show unexported fields so nobody tries to make their own instance ;)
+}
+
+func (ExtraPayloads[SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
+ v := pseudo.MustNewValue[SA](s.t)
+ return &StateAccountExtra{
+ t: pseudo.From(v.Get()).Type,
+ }
+}
+
+// FromStateAccount returns the StateAccount's payload.
+func (ExtraPayloads[SA]) FromStateAccount(a *StateAccount) SA {
+ return pseudo.MustNewValue[SA](a.extra().payload()).Get()
+}
+
+// PointerFromStateAccount returns a pointer to the StateAccounts's extra
+// payload. This is guaranteed to be non-nil.
+//
+// Note that copying a StateAccount by dereferencing a pointer will result in a
+// shallow copy and that the *SA returned here will therefore be shared by all
+// copies. If this is not the desired behaviour, use
+// [StateAccount.Copy] or [ExtraPayloads.SetOnStateAccount].
+func (ExtraPayloads[SA]) PointerFromStateAccount(a *StateAccount) *SA {
+ return pseudo.MustPointerTo[SA](a.extra().payload()).Value.Get()
+}
+
+// SetOnStateAccount sets the StateAccount's payload.
+func (ExtraPayloads[SA]) SetOnStateAccount(a *StateAccount, val SA) {
+ a.extra().t = pseudo.From(val).Type
+}
+
+// A StateAccountExtra carries the extra payload, if any, registered with
+// [RegisterExtras]. It SHOULD NOT be used directly; instead use the
+// [ExtraPayloads] accessor returned by RegisterExtras.
+type StateAccountExtra struct {
+ t *pseudo.Type
+}
+
+func (a *StateAccount) extra() *StateAccountExtra {
+ if a.Extra == nil {
+ a.Extra = &StateAccountExtra{
+ t: registeredExtras.newStateAccount(),
+ }
+ }
+ return a.Extra
+}
+
+func (e *StateAccountExtra) payload() *pseudo.Type {
+ if e.t == nil {
+ e.t = registeredExtras.newStateAccount()
+ }
+ 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
+ fmt.Formatter
+} = (*StateAccountExtra)(nil)
+
+// EncodeRLP implements the [rlp.Encoder] interface.
+func (e *StateAccountExtra) EncodeRLP(w io.Writer) error {
+ switch r := registeredExtras; {
+ case r == nil:
+ return nil
+ case e == nil:
+ e = &StateAccountExtra{}
+ fallthrough
+ case e.t == nil:
+ e.t = r.newStateAccount()
+ }
+ return e.t.EncodeRLP(w)
+}
+
+// DecodeRLP implements the [rlp.Decoder] interface.
+func (e *StateAccountExtra) DecodeRLP(s *rlp.Stream) error {
+ switch r := registeredExtras; {
+ case r == nil:
+ return nil
+ case e.t == nil:
+ e.t = r.newStateAccount()
+ fallthrough
+ default:
+ return s.Decode(e.t)
+ }
+}
+
+// Format implements the [fmt.Formatter] interface.
+func (e *StateAccountExtra) Format(s fmt.State, verb rune) {
+ var out string
+ switch r := registeredExtras; {
+ case r == nil:
+ out = ""
+ case e == nil, e.t == nil:
+ out = fmt.Sprintf("[*StateAccountExtra[%s]]", r.stateAccountType)
+ default:
+ e.t.Format(s, verb)
+ return
+ }
+ _, _ = s.Write([]byte(out))
+}
diff --git a/core/types/state_account.go b/core/types/state_account.go
index 52ef843b3527..3de3d4f64ee0 100644
--- a/core/types/state_account.go
+++ b/core/types/state_account.go
@@ -25,6 +25,7 @@ import (
)
//go:generate go run ../../rlp/rlpgen -type StateAccount -out gen_account_rlp.go
+//go:generate go run ../../rlp/rlpgen -type SlimAccount -out gen_slim_account_rlp.libevm.go
// StateAccount is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
@@ -33,6 +34,8 @@ type StateAccount struct {
Balance *uint256.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
+
+ Extra *StateAccountExtra
}
// NewEmptyStateAccount constructs an empty state account.
@@ -55,6 +58,7 @@ func (acct *StateAccount) Copy() *StateAccount {
Balance: balance,
Root: acct.Root,
CodeHash: common.CopyBytes(acct.CodeHash),
+ Extra: acct.Extra.clone(),
}
}
@@ -66,6 +70,8 @@ type SlimAccount struct {
Balance *uint256.Int
Root []byte // Nil if root equals to types.EmptyRootHash
CodeHash []byte // Nil if hash equals to types.EmptyCodeHash
+
+ Extra *StateAccountExtra
}
// SlimAccountRLP encodes the state account in 'slim RLP' format.
@@ -73,6 +79,7 @@ func SlimAccountRLP(account StateAccount) []byte {
slim := SlimAccount{
Nonce: account.Nonce,
Balance: account.Balance,
+ Extra: account.Extra,
}
if account.Root != EmptyRootHash {
slim.Root = account.Root[:]
@@ -80,7 +87,7 @@ func SlimAccountRLP(account StateAccount) []byte {
if !bytes.Equal(account.CodeHash, EmptyCodeHash[:]) {
slim.CodeHash = account.CodeHash
}
- data, err := rlp.EncodeToBytes(slim)
+ data, err := rlp.EncodeToBytes(&slim)
if err != nil {
panic(err)
}
@@ -96,6 +103,7 @@ func FullAccount(data []byte) (*StateAccount, error) {
}
var account StateAccount
account.Nonce, account.Balance = slim.Nonce, slim.Balance
+ account.Extra = slim.Extra
// Interpret the storage root and code hash in slim format.
if len(slim.Root) == 0 {
diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go
new file mode 100644
index 000000000000..c7fe6d506213
--- /dev/null
+++ b/core/types/state_account.libevm_test.go
@@ -0,0 +1,214 @@
+// 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
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/holiman/uint256"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/libevm/pseudo"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+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
+ // default behaviour. Encodings that involve a boolean payload were
+ // generated on ava-labs/coreth StateAccounts to guarantee equivalence.
+
+ type test struct {
+ name string
+ register func()
+ acc *StateAccount
+ wantHex string
+ }
+
+ explicitFalseBoolean := test{
+ name: "explicit false-boolean extra",
+ register: func() {
+ RegisterExtras[bool]()
+ },
+ acc: &StateAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x666666),
+ Root: common.Hash{},
+ CodeHash: []byte{0xbb, 0xbb, 0xbb},
+ Extra: &StateAccountExtra{
+ t: pseudo.From(false).Type,
+ },
+ },
+ wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb80`,
+ }
+
+ // The vanilla geth code won't set payloads so we need to ensure that the
+ // zero-value encoding is used instead of the null-value default as when
+ // no type is registered.
+ implicitFalseBoolean := explicitFalseBoolean
+ implicitFalseBoolean.name = "implicit false-boolean extra as zero-value of registered type"
+ // Clearing the Extra makes the `false` value implicit and due only to the
+ // fact that we register `bool`. Most importantly, note that `wantHex`
+ // remains identical.
+ implicitFalseBoolean.acc.Extra = nil
+
+ tests := []test{
+ explicitFalseBoolean,
+ implicitFalseBoolean,
+ {
+ name: "true-boolean extra",
+ register: func() {
+ RegisterExtras[bool]()
+ },
+ acc: &StateAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x666666),
+ Root: common.Hash{},
+ CodeHash: []byte{0xbb, 0xbb, 0xbb},
+ Extra: &StateAccountExtra{
+ t: pseudo.From(true).Type,
+ },
+ },
+ wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb01`,
+ },
+ {
+ name: "vanilla geth account",
+ acc: &StateAccount{
+ Nonce: 0xcccccc,
+ Balance: uint256.NewInt(0x555555),
+ Root: common.MaxHash,
+ CodeHash: []byte{0x77, 0x77, 0x77},
+ },
+ wantHex: `0xed83cccccc83555555a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83777777`,
+ },
+ {
+ name: "vanilla geth account",
+ acc: &StateAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x666666),
+ Root: common.Hash{},
+ CodeHash: []byte{0xbb, 0xbb, 0xbb},
+ },
+ wantHex: `0xed8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.register != nil {
+ TestOnlyClearRegisteredExtras()
+ tt.register()
+ t.Cleanup(TestOnlyClearRegisteredExtras)
+ }
+ assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex)
+
+ t.Run("RLP round trip via SlimAccount", func(t *testing.T) {
+ got, err := FullAccount(SlimAccountRLP(*tt.acc))
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(tt.acc, got); diff != "" {
+ t.Errorf("FullAccount(SlimAccountRLP(x)) != x; diff (-want +got):\n%s", diff)
+ }
+ })
+ })
+ }
+}
+
+func assertRLPEncodingAndReturn(t *testing.T, val any, wantHex string) []byte {
+ t.Helper()
+ got, err := rlp.EncodeToBytes(val)
+ require.NoError(t, err, "rlp.EncodeToBytes()")
+
+ t.Logf("got RLP: %#x", got)
+ wantHex = strings.TrimPrefix(wantHex, "0x")
+ require.Equalf(t, common.Hex2Bytes(wantHex), got, "RLP encoding of %T", val)
+
+ return got
+}
+
+func TestSlimAccountRLP(t *testing.T) {
+ // All RLP encodings were generated on geth SlimAccounts *before* libevm
+ // modifications, to lock in default behaviour.
+ tests := []struct {
+ name string
+ acc *SlimAccount
+ wantHex string
+ }{
+ {
+ acc: &SlimAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x777777),
+ },
+ wantHex: `0xca83444444837777778080`,
+ },
+ {
+ acc: &SlimAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x777777),
+ Root: common.MaxHash[:],
+ },
+ wantHex: `0xea8344444483777777a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80`,
+ },
+ {
+ acc: &SlimAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x777777),
+ CodeHash: common.MaxHash[:],
+ },
+ wantHex: `0xea834444448377777780a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`,
+ },
+ {
+ acc: &SlimAccount{
+ Nonce: 0x444444,
+ Balance: uint256.NewInt(0x777777),
+ Root: common.MaxHash[:],
+ CodeHash: repeatAsHash(0xee).Bytes(),
+ },
+ wantHex: `0xf84a8344444483777777a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex)
+
+ got := new(SlimAccount)
+ require.NoError(t, rlp.DecodeBytes(buf, got), "rlp.DecodeBytes()")
+
+ opts := []cmp.Option{
+ // The require package differentiates between empty and nil
+ // slices and doesn't have a configuration mechanism.
+ cmpopts.EquateEmpty(),
+ }
+
+ if diff := cmp.Diff(tt.acc, got, opts...); diff != "" {
+ t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%T), ...) round trip; diff (-want +got):\n%s", tt.acc, diff)
+ }
+ })
+ }
+}
+
+func repeatAsHash(x byte) (h common.Hash) {
+ for i := range h {
+ h[i] = x
+ }
+ return h
+}
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 000000000000..e32f065293c6
--- /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/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go
index fb2a7a1967ef..0ba3aef5e072 100644
--- a/core/vm/contracts.libevm.go
+++ b/core/vm/contracts.libevm.go
@@ -38,7 +38,7 @@ import (
// args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil /*value*/}
type evmCallArgs struct {
evm *EVM
- callType callType
+ callType CallType
// args:start
caller ContractRef
@@ -49,15 +49,32 @@ type evmCallArgs struct {
// args:end
}
-type callType uint8
+// A CallType refers to a *CALL* [OpCode] / respective method on [EVM].
+type CallType uint8
const (
- call callType = iota + 1
- callCode
- delegateCall
- staticCall
+ UnknownCallType CallType = iota
+ Call
+ CallCode
+ DelegateCall
+ StaticCall
)
+// String returns a human-readable representation of the CallType.
+func (t CallType) String() string {
+ switch t {
+ case Call:
+ return "Call"
+ case CallCode:
+ return "CallCode"
+ case DelegateCall:
+ return "DelegateCall"
+ case StaticCall:
+ return "StaticCall"
+ }
+ return fmt.Sprintf("Unknown %T(%d)", t, t)
+}
+
// run runs the [PrecompiledContract], differentiating between stateful and
// regular types.
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
@@ -105,6 +122,7 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) {
type PrecompileEnvironment interface {
Environment
+ IncomingCallType() CallType
// Call is equivalent to [EVM.Call] except that the `caller` argument is
// removed and automatically determined according to the type of call that
// invoked the precompile.
@@ -117,46 +135,30 @@ func (args *evmCallArgs) env() *environment {
value = args.value
)
switch args.callType {
- case staticCall:
+ case StaticCall:
value = new(uint256.Int)
fallthrough
- case call:
+ case Call:
self = args.addr
- case delegateCall:
+ case DelegateCall:
value = nil
fallthrough
- case callCode:
+ case CallCode:
self = args.caller.Address()
}
// This is equivalent to the `contract` variables created by evm.*Call*()
// methods, for non precompiles, to pass to [EVMInterpreter.Run].
contract := NewContract(args.caller, AccountRef(self), value, args.gas)
- if args.callType == delegateCall {
+ if args.callType == DelegateCall {
contract = contract.AsDelegate()
}
return &environment{
- evm: args.evm,
- self: contract,
- forceReadOnly: args.readOnly(),
- }
-}
-
-func (args *evmCallArgs) readOnly() bool {
- // A switch statement provides clearer code coverage for difficult-to-test
- // cases.
- switch {
- case args.callType == staticCall:
- // evm.interpreter.readOnly is only set to true via a call to
- // EVMInterpreter.Run() so, if a precompile is called directly with
- // StaticCall(), then readOnly might not be set yet.
- return true
- case args.evm.interpreter.readOnly:
- return true
- default:
- return false
+ evm: args.evm,
+ self: contract,
+ callType: args.callType,
}
}
diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go
index 614d96947fa6..ed281c349862 100644
--- a/core/vm/contracts.libevm_test.go
+++ b/core/vm/contracts.libevm_test.go
@@ -16,6 +16,7 @@
package vm_test
import (
+ "bytes"
"fmt"
"math/big"
"reflect"
@@ -106,6 +107,7 @@ type statefulPrecompileOutput struct {
BlockNumber, Difficulty *big.Int
BlockTime uint64
Input []byte
+ IncomingCallType vm.CallType
}
func (o statefulPrecompileOutput) String() string {
@@ -121,6 +123,8 @@ func (o statefulPrecompileOutput) String() string {
verb = "%#x"
case *libevm.AddressContext:
verb = "%+v"
+ case vm.CallType:
+ verb = "%d (%[2]q)"
}
lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld))
}
@@ -149,14 +153,15 @@ func TestNewStatefulPrecompile(t *testing.T) {
}
out := &statefulPrecompileOutput{
- ChainID: env.ChainConfig().ChainID,
- Addresses: env.Addresses(),
- StateValue: env.ReadOnlyState().GetState(precompile, slot),
- ReadOnly: env.ReadOnly(),
- BlockNumber: env.BlockNumber(),
- BlockTime: env.BlockTime(),
- Difficulty: hdr.Difficulty,
- Input: input,
+ ChainID: env.ChainConfig().ChainID,
+ Addresses: env.Addresses(),
+ StateValue: env.ReadOnlyState().GetState(precompile, slot),
+ ReadOnly: env.ReadOnly(),
+ BlockNumber: env.BlockNumber(),
+ BlockTime: env.BlockTime(),
+ Difficulty: hdr.Difficulty,
+ Input: input,
+ IncomingCallType: env.IncomingCallType(),
}
return out.Bytes(), suppliedGas - gasCost, nil
}
@@ -199,6 +204,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
// Note that this only covers evm.readOnly being true because of the
// precompile's call. See TestInheritReadOnly for alternate case.
wantReadOnly bool
+ wantCallType vm.CallType
}{
{
name: "EVM.Call()",
@@ -211,6 +217,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
Self: precompile,
},
wantReadOnly: false,
+ wantCallType: vm.Call,
},
{
name: "EVM.CallCode()",
@@ -223,6 +230,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
Self: caller,
},
wantReadOnly: false,
+ wantCallType: vm.CallCode,
},
{
name: "EVM.DelegateCall()",
@@ -235,6 +243,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
Self: caller,
},
wantReadOnly: false,
+ wantCallType: vm.DelegateCall,
},
{
name: "EVM.StaticCall()",
@@ -247,20 +256,22 @@ func TestNewStatefulPrecompile(t *testing.T) {
Self: precompile,
},
wantReadOnly: true,
+ wantCallType: vm.StaticCall,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantOutput := statefulPrecompileOutput{
- ChainID: chainID,
- Addresses: tt.wantAddresses,
- StateValue: value,
- ReadOnly: tt.wantReadOnly,
- BlockNumber: header.Number,
- BlockTime: header.Time,
- Difficulty: header.Difficulty,
- Input: input,
+ ChainID: chainID,
+ Addresses: tt.wantAddresses,
+ StateValue: value,
+ ReadOnly: tt.wantReadOnly,
+ BlockNumber: header.Number,
+ BlockTime: header.Time,
+ Difficulty: header.Difficulty,
+ Input: input,
+ IncomingCallType: tt.wantCallType,
}
wantGasLeft := gasLimit - gasCost
@@ -375,10 +386,12 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC
t.Helper()
const p0 = vm.PUSH0
contract := []vm.OpCode{
- vm.PUSH1, 1, // retSize (bytes)
- p0, // retOffset
- p0, // argSize
- p0, // argOffset
+ vm.CALLDATASIZE, p0, p0, vm.CALLDATACOPY,
+
+ p0, // retSize
+ p0, // retOffset
+ vm.CALLDATASIZE, // argSize
+ p0, // argOffset
}
// See CALL signature: https://www.evm.codes/#f1?fork=cancun
@@ -511,13 +524,21 @@ func TestPrecompileMakeCall(t *testing.T) {
dest := common.HexToAddress("DE57")
rng := ethtest.NewPseudoRand(142857)
- callData := rng.Bytes(8)
+ precompileCallData := rng.Bytes(8)
+
+ // If the SUT precompile receives this as its calldata then it will use the
+ // vm.WithUNSAFECallerAddressProxying() option.
+ unsafeCallerProxyOptSentinel := []byte("override-caller sentinel")
hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
+ var opts []vm.CallOption
+ if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
+ opts = append(opts, vm.WithUNSAFECallerAddressProxying())
+ }
// We are ultimately testing env.Call(), hence why this is the SUT.
- return env.Call(dest, callData, suppliedGas, uint256.NewInt(0))
+ return env.Call(dest, precompileCallData, suppliedGas, uint256.NewInt(0), opts...)
}),
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
out := &statefulPrecompileOutput{
@@ -538,6 +559,7 @@ func TestPrecompileMakeCall(t *testing.T) {
tests := []struct {
incomingCallType vm.OpCode
+ eoaTxCallData []byte
// Unlike TestNewStatefulPrecompile, which tests the AddressContext of
// the precompile itself, these test the AddressContext of a contract
// called by the precompile.
@@ -551,7 +573,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: sut,
Self: dest,
},
- Input: callData,
+ Input: precompileCallData,
+ },
+ },
+ {
+ incomingCallType: vm.CALL,
+ eoaTxCallData: unsafeCallerProxyOptSentinel,
+ want: statefulPrecompileOutput{
+ Addresses: &libevm.AddressContext{
+ Origin: eoa,
+ Caller: caller, // overridden by CallOption
+ Self: dest,
+ },
+ Input: precompileCallData,
},
},
{
@@ -562,7 +596,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
},
- Input: callData,
+ Input: precompileCallData,
+ },
+ },
+ {
+ incomingCallType: vm.CALLCODE,
+ eoaTxCallData: unsafeCallerProxyOptSentinel,
+ want: statefulPrecompileOutput{
+ Addresses: &libevm.AddressContext{
+ Origin: eoa,
+ Caller: caller, // CallOption is a NOOP
+ Self: dest,
+ },
+ Input: precompileCallData,
},
},
{
@@ -573,7 +619,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: caller, // as with CALLCODE
Self: dest,
},
- Input: callData,
+ Input: precompileCallData,
+ },
+ },
+ {
+ incomingCallType: vm.DELEGATECALL,
+ eoaTxCallData: unsafeCallerProxyOptSentinel,
+ want: statefulPrecompileOutput{
+ Addresses: &libevm.AddressContext{
+ Origin: eoa,
+ Caller: caller, // CallOption is a NOOP
+ Self: dest,
+ },
+ Input: precompileCallData,
},
},
{
@@ -584,7 +642,7 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: sut,
Self: dest,
},
- Input: callData,
+ Input: precompileCallData,
// This demonstrates that even though the precompile makes a
// (non-static) CALL, the read-only state is inherited. Yes,
// this is _another_ way to get a read-only state, different to
@@ -592,19 +650,56 @@ func TestPrecompileMakeCall(t *testing.T) {
ReadOnly: true,
},
},
+ {
+ incomingCallType: vm.STATICCALL,
+ eoaTxCallData: unsafeCallerProxyOptSentinel,
+ want: statefulPrecompileOutput{
+ Addresses: &libevm.AddressContext{
+ Origin: eoa,
+ Caller: caller, // overridden by CallOption
+ Self: dest,
+ },
+ Input: precompileCallData,
+ ReadOnly: true,
+ },
+ },
}
for _, tt := range tests {
- t.Run(fmt.Sprintf("via %s", tt.incomingCallType), func(t *testing.T) {
+ t.Run(tt.incomingCallType.String(), func(t *testing.T) {
+ t.Logf("calldata = %q", tt.eoaTxCallData)
state, evm := ethtest.NewZeroEVM(t)
evm.Origin = eoa
state.CreateAccount(caller)
proxy := makeReturnProxy(t, sut, tt.incomingCallType)
state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy))
- got, _, err := evm.Call(vm.AccountRef(eoa), caller, nil, 1e6, uint256.NewInt(0))
+ got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0))
require.NoError(t, err)
require.Equal(t, tt.want.String(), string(got))
})
}
}
+
+//nolint:testableexamples // Including output would only make the example more complicated and hide the true intent
+func ExamplePrecompileEnvironment() {
+ // To determine the actual caller of a precompile, as against the effective
+ // caller (under EVM rules, as exposed by `Addresses().Caller`):
+ actualCaller := func(env vm.PrecompileEnvironment) common.Address {
+ if env.IncomingCallType() == vm.DelegateCall {
+ // DelegateCall acts as if it were its own caller.
+ return env.Addresses().Self
+ }
+ // CallCode could return either `Self` or `Caller` as it acts as its
+ // caller but doesn't inherit the caller's caller as DelegateCall does.
+ // Having it handled here is arbitrary from a behavioural perspective
+ // and is done only to simplify the code.
+ //
+ // Call and StaticCall don't affect self/caller semantics in any way.
+ return env.Addresses().Caller
+ }
+
+ // actualCaller would typically be a top-level function. It's only a
+ // variable to include it in this example function.
+ _ = actualCaller
+}
diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go
index 92536f19f878..18dd116d7f51 100644
--- a/core/vm/environment.libevm.go
+++ b/core/vm/environment.libevm.go
@@ -28,8 +28,8 @@ import (
"github.com/ethereum/go-ethereum/params"
)
-// An Environment provides information about the context in which a precompiled
-// contract or an instruction is being executed.
+// An Environment provides information about the context in which an instruction
+// is being executed.
type Environment interface {
ChainConfig() *params.ChainConfig
Rules() params.Rules
@@ -48,18 +48,34 @@ type Environment interface {
var _ PrecompileEnvironment = (*environment)(nil)
type environment struct {
- evm *EVM
- self *Contract
- forceReadOnly bool
+ evm *EVM
+ self *Contract
+ callType CallType
}
func (e *environment) ChainConfig() *params.ChainConfig { return e.evm.chainConfig }
func (e *environment) Rules() params.Rules { return e.evm.chainRules }
-func (e *environment) ReadOnly() bool { return e.forceReadOnly || e.evm.interpreter.readOnly }
func (e *environment) ReadOnlyState() libevm.StateReader { return e.evm.StateDB }
+func (e *environment) IncomingCallType() CallType { return e.callType }
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
+func (e *environment) ReadOnly() bool {
+ // A switch statement provides clearer code coverage for difficult-to-test
+ // cases.
+ switch {
+ case e.callType == StaticCall:
+ // evm.interpreter.readOnly is only set to true via a call to
+ // EVMInterpreter.Run() so, if a precompile is called directly with
+ // StaticCall(), then readOnly might not be set yet.
+ return true
+ case e.evm.interpreter.readOnly:
+ return true
+ default:
+ return false
+ }
+}
+
func (e *environment) Addresses() *libevm.AddressContext {
return &libevm.AddressContext{
Origin: e.evm.Origin,
@@ -88,10 +104,10 @@ func (e *environment) BlockHeader() (types.Header, error) {
}
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
- return e.callContract(call, addr, input, gas, value, opts...)
+ return e.callContract(Call, addr, input, gas, value, opts...)
}
-func (e *environment) callContract(typ callType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
+func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
// isn't used for precompiles, so we need to do it ourselves to maintain the
// expected invariants.
@@ -100,7 +116,7 @@ func (e *environment) callContract(typ callType, addr common.Address, input []by
in.evm.depth++
defer func() { in.evm.depth-- }()
- if e.forceReadOnly && !in.readOnly { // i.e. the precompile was StaticCall()ed
+ if e.ReadOnly() && !in.readOnly { // i.e. the precompile was StaticCall()ed
in.readOnly = true
defer func() { in.readOnly = false }()
}
@@ -112,6 +128,11 @@ func (e *environment) callContract(typ callType, addr common.Address, input []by
// Note that, in addition to being unsafe, this breaks an EVM
// assumption that the caller ContractRef is always a *Contract.
caller = AccountRef(e.self.CallerAddress)
+ if e.callType == DelegateCall {
+ // self was created with AsDelegate(), which means that
+ // CallerAddress was inherited.
+ caller = AccountRef(e.self.Address())
+ }
case nil:
default:
return nil, gas, fmt.Errorf("unsupported option %T", o)
@@ -119,12 +140,12 @@ func (e *environment) callContract(typ callType, addr common.Address, input []by
}
switch typ {
- case call:
+ case Call:
if in.readOnly && !value.IsZero() {
return nil, gas, ErrWriteProtection
}
return e.evm.Call(caller, addr, input, gas, value)
- case callCode, delegateCall, staticCall:
+ case CallCode, DelegateCall, StaticCall:
// TODO(arr4n): these cases should be very similar to CALL, hence the
// early abstraction, to signal to future maintainers. If implementing
// them, there's likely no need to honour the
diff --git a/core/vm/evm.go b/core/vm/evm.go
index c6c735627fd4..813a33414abc 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -230,7 +230,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
}
if isPrecompile {
- args := &evmCallArgs{evm, call, caller, addr, input, gas, value}
+ args := &evmCallArgs{evm, Call, caller, addr, input, gas, value}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
@@ -294,7 +294,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- args := &evmCallArgs{evm, callCode, caller, addr, input, gas, value}
+ args := &evmCallArgs{evm, CallCode, caller, addr, input, gas, value}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
@@ -340,7 +340,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- args := &evmCallArgs{evm, delegateCall, caller, addr, input, gas, nil}
+ args := &evmCallArgs{evm, DelegateCall, caller, addr, input, gas, nil}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
@@ -390,7 +390,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}
if p, isPrecompile := evm.precompile(addr); isPrecompile {
- args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil}
+ args := &evmCallArgs{evm, StaticCall, caller, addr, input, gas, nil}
ret, gas, err = args.RunPrecompiledContract(p, input, gas)
} else {
// At this point, we use a copy of address. If we don't, the go compiler will
diff --git a/core/vm/jump_table.libevm.go b/core/vm/jump_table.libevm.go
index bbd1ad83ba50..1bf04f1d3f0d 100644
--- a/core/vm/jump_table.libevm.go
+++ b/core/vm/jump_table.libevm.go
@@ -49,6 +49,11 @@ func (fn OperationFunc) internal() executionFunc {
env := &environment{
evm: interpreter.evm,
self: scope.Contract,
+ // The CallType isn't exposed by an instruction's [Environment] and,
+ // although [UnknownCallType] is the default value, it's explicitly
+ // set to avoid future accidental setting without proper
+ // justification.
+ callType: UnknownCallType,
}
return fn(env, pc, interpreter, scope)
}
diff --git a/libevm/ethtest/rand.go b/libevm/ethtest/rand.go
index 8584ce11698f..eb7f47fda02c 100644
--- a/libevm/ethtest/rand.go
+++ b/libevm/ethtest/rand.go
@@ -13,11 +13,13 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// .
+
package ethtest
import (
"math/big"
+ "github.com/holiman/uint256"
"golang.org/x/exp/rand"
"github.com/ethereum/go-ethereum/common"
@@ -33,9 +35,16 @@ func NewPseudoRand(seed uint64) *PseudoRand {
return &PseudoRand{rand.New(rand.NewSource(seed))}
}
+// Read is equivalent to [rand.Rand.Read] except that it doesn't return an error
+// because it is guaranteed to be nil.
+func (r *PseudoRand) Read(p []byte) int {
+ n, _ := r.Rand.Read(p) // Guaranteed nil error
+ return n
+}
+
// Address returns a pseudorandom address.
func (r *PseudoRand) Address() (a common.Address) {
- r.Read(a[:]) //nolint:gosec,errcheck // Guaranteed nil error
+ r.Read(a[:])
return a
}
@@ -47,14 +56,20 @@ func (r *PseudoRand) AddressPtr() *common.Address {
// Hash returns a pseudorandom hash.
func (r *PseudoRand) Hash() (h common.Hash) {
- r.Read(h[:]) //nolint:gosec,errcheck // Guaranteed nil error
+ r.Read(h[:])
return h
}
+// HashPtr returns a pointer to a pseudorandom hash.
+func (r *PseudoRand) HashPtr() *common.Hash {
+ h := r.Hash()
+ return &h
+}
+
// Bytes returns `n` pseudorandom bytes.
func (r *PseudoRand) Bytes(n uint) []byte {
b := make([]byte, n)
- r.Read(b) //nolint:gosec,errcheck // Guaranteed nil error
+ r.Read(b)
return b
}
@@ -62,3 +77,14 @@ func (r *PseudoRand) Bytes(n uint) []byte {
func (r *PseudoRand) BigUint64() *big.Int {
return new(big.Int).SetUint64(r.Uint64())
}
+
+// Uint64Ptr returns a pointer to a pseudorandom uint64.
+func (r *PseudoRand) Uint64Ptr() *uint64 {
+ u := r.Uint64()
+ return &u
+}
+
+// Uint256 returns a random 256-bit unsigned int.
+func (r *PseudoRand) Uint256() *uint256.Int {
+ return new(uint256.Int).SetBytes(r.Bytes(32))
+}
diff --git a/libevm/pseudo/constructor.go b/libevm/pseudo/constructor.go
index 424572785721..d72237494dea 100644
--- a/libevm/pseudo/constructor.go
+++ b/libevm/pseudo/constructor.go
@@ -13,6 +13,7 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// .
+
package pseudo
// A Constructor returns newly constructed [Type] instances for a pre-registered
diff --git a/libevm/pseudo/fmt.go b/libevm/pseudo/fmt.go
new file mode 100644
index 000000000000..e10234e6783e
--- /dev/null
+++ b/libevm/pseudo/fmt.go
@@ -0,0 +1,56 @@
+// 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 pseudo
+
+import (
+ "fmt"
+)
+
+var _ = []fmt.Formatter{
+ (*Type)(nil),
+ (*Value[struct{}])(nil),
+ (*concrete[struct{}])(nil),
+}
+
+// Format implements the [fmt.Formatter] interface.
+func (t *Type) Format(s fmt.State, verb rune) {
+ switch {
+ case t == nil, t.val == nil:
+ writeToFmtState(s, "[pseudo.Type[unknown]]")
+ default:
+ t.val.Format(s, verb)
+ }
+}
+
+// Format implements the [fmt.Formatter] interface.
+func (v *Value[T]) Format(s fmt.State, verb rune) { v.t.Format(s, verb) }
+
+func (c *concrete[T]) Format(s fmt.State, verb rune) {
+ switch {
+ case c == nil:
+ writeToFmtState(s, "[pseudo.Type[%T]]", concrete[T]{}.val)
+ default:
+ // Respects the original formatting directive. fmt all the way down!
+ format := fmt.Sprintf("pseudo.Type[%%T]{%s}", fmt.FormatString(s, verb))
+ writeToFmtState(s, format, c.val, c.val)
+ }
+}
+
+func writeToFmtState(s fmt.State, format string, a ...any) {
+ // There is no way to bubble errors out from a `fmt.Formatter`.
+ _, _ = s.Write([]byte(fmt.Sprintf(format, a...)))
+}
diff --git a/libevm/pseudo/fmt_test.go b/libevm/pseudo/fmt_test.go
new file mode 100644
index 000000000000..e29ecab0565f
--- /dev/null
+++ b/libevm/pseudo/fmt_test.go
@@ -0,0 +1,82 @@
+// 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 pseudo
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFormat(t *testing.T) {
+ tests := []struct {
+ name string
+ from any
+ format string
+ wantContains []string
+ }{
+ {
+ name: "width",
+ from: 42,
+ format: "%04d",
+ wantContains: []string{"int", "0042"},
+ },
+ {
+ name: "precision",
+ from: float64(2),
+ format: "%.5f",
+ wantContains: []string{"float64", "2.00000"},
+ },
+ {
+ name: "flag",
+ from: 42,
+ format: "%+d",
+ wantContains: []string{"int", "+42"},
+ },
+ {
+ name: "verb",
+ from: 42,
+ format: "%x",
+ wantContains: []string{"int", "2a"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := fmt.Sprintf(tt.format, fromAny(t, tt.from))
+ for _, want := range tt.wantContains {
+ assert.Containsf(t, got, want, "fmt.Sprintf(%q, From(%T[%[2]v]))", tt.format, tt.from)
+ }
+ })
+ }
+}
+
+func fromAny(t *testing.T, x any) *Type {
+ t.Helper()
+
+ // Without this, the function will be From[any]().
+ switch x := x.(type) {
+ case int:
+ return From(x).Type
+ case float64:
+ return From(x).Type
+ default:
+ t.Fatalf("Bad test setup: add type case for %T", x)
+ return nil
+ }
+}
diff --git a/libevm/pseudo/reflect.go b/libevm/pseudo/reflect.go
new file mode 100644
index 000000000000..45f5a940ea0b
--- /dev/null
+++ b/libevm/pseudo/reflect.go
@@ -0,0 +1,60 @@
+// 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 pseudo
+
+import (
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// Reflection is used as a last resort in pseudo types so is limited to this
+// file to avoid being seen as the norm. If you are adding to this file, please
+// try to achieve the same results with type parameters.
+
+func (c *concrete[T]) isZero() bool {
+ // The alternative would require that T be comparable, which would bubble up
+ // and invade the rest of the code base.
+ return reflect.ValueOf(c.val).IsZero()
+}
+
+func (c *concrete[T]) equal(t *Type) bool {
+ d, ok := t.val.(*concrete[T])
+ if !ok {
+ return false
+ }
+ switch v := any(c.val).(type) {
+ case EqualityChecker[T]:
+ return v.Equal(d.val)
+ default:
+ // See rationale for reflection in [concrete.isZero].
+ return reflect.DeepEqual(c.val, d.val)
+ }
+}
+
+func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error {
+ switch v := reflect.ValueOf(c.val); v.Kind() {
+ case reflect.Pointer:
+ if v.IsNil() {
+ el := v.Type().Elem()
+ c.val = reflect.New(el).Interface().(T) //nolint:forcetypeassert // Invariant scoped to the last few lines of code so simple to verify
+ }
+ return s.Decode(c.val)
+ default:
+ return s.Decode(&c.val)
+ }
+}
diff --git a/libevm/pseudo/rlp_test.go b/libevm/pseudo/rlp_test.go
new file mode 100644
index 000000000000..8d72bd6de1ff
--- /dev/null
+++ b/libevm/pseudo/rlp_test.go
@@ -0,0 +1,86 @@
+// 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 pseudo_test
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/libevm/ethtest"
+ "github.com/ethereum/go-ethereum/libevm/pseudo"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+func TestRLPEquivalence(t *testing.T) {
+ t.Parallel()
+
+ for seed := uint64(0); seed < 20; seed++ {
+ seed := seed
+
+ t.Run("fuzz pointer-type round trip", func(t *testing.T) {
+ t.Parallel()
+ rng := ethtest.NewPseudoRand(seed)
+
+ hdr := &types.Header{
+ ParentHash: rng.Hash(),
+ UncleHash: rng.Hash(),
+ Coinbase: rng.Address(),
+ Root: rng.Hash(),
+ TxHash: rng.Hash(),
+ ReceiptHash: rng.Hash(),
+ Difficulty: big.NewInt(rng.Int63()),
+ Number: big.NewInt(rng.Int63()),
+ GasLimit: rng.Uint64(),
+ GasUsed: rng.Uint64(),
+ Time: rng.Uint64(),
+ Extra: rng.Bytes(uint(rng.Uint64n(128))),
+ MixDigest: rng.Hash(),
+ }
+ rng.Read(hdr.Bloom[:])
+ rng.Read(hdr.Nonce[:])
+
+ want, err := rlp.EncodeToBytes(hdr)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", hdr)
+
+ typ := pseudo.From(hdr).Type
+ gotRLP, err := rlp.EncodeToBytes(typ)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", typ)
+
+ require.Equalf(t, want, gotRLP, "RLP encoding of %T (canonical) vs %T (under test)", hdr, typ)
+
+ t.Run("decode", func(t *testing.T) {
+ pseudo := pseudo.Zero[*types.Header]()
+ require.NoErrorf(t, rlp.DecodeBytes(gotRLP, pseudo.Type), "rlp.DecodeBytes(..., %T[%T])", pseudo.Type, hdr)
+ require.Equal(t, hdr, pseudo.Value.Get(), "RLP-decoded value")
+ })
+ })
+
+ t.Run("fuzz non-pointer decode", func(t *testing.T) {
+ rng := ethtest.NewPseudoRand(seed)
+ x := rng.Uint64()
+ buf, err := rlp.EncodeToBytes(x)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", x)
+
+ pseudo := pseudo.Zero[uint64]()
+ require.NoErrorf(t, rlp.DecodeBytes(buf, pseudo.Type), "rlp.DecodeBytes(..., %T[%T])", pseudo.Type, x)
+ require.Equal(t, x, pseudo.Value.Get(), "RLP-decoded value")
+ })
+ }
+}
diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go
index 21eb31e2aa2f..6b80e18a9674 100644
--- a/libevm/pseudo/type.go
+++ b/libevm/pseudo/type.go
@@ -31,6 +31,9 @@ package pseudo
import (
"encoding/json"
"fmt"
+ "io"
+
+ "github.com/ethereum/go-ethereum/rlp"
)
// A Type wraps a strongly-typed value without exposing information about its
@@ -121,6 +124,21 @@ func MustNewValue[T any](t *Type) *Value[T] {
return v
}
+// IsZero reports whether t carries the the zero value for its type.
+func (t *Type) IsZero() bool { return t.val.isZero() }
+
+// An EqualityChecker reports if it is equal to another value of the same type.
+type EqualityChecker[T any] interface {
+ Equal(T) bool
+}
+
+// Equal reports whether t carries a value equal to that carried by u. If t and
+// u carry different types then Equal returns false. If t and u carry the same
+// type and said type implements [EqualityChecker] then Equal propagates the
+// value returned by the checker. In all other cases, Equal returns
+// [reflect.DeepEqual] performed on the payloads carried by t and u.
+func (t *Type) Equal(u *Type) bool { return t.val.equal(u) }
+
// Get returns the value.
func (v *Value[T]) Get() T { return v.t.val.get().(T) } //nolint:forcetypeassert // invariant
@@ -139,6 +157,12 @@ func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() }
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) }
+// EncodeRLP implements the [rlp.Encoder] interface.
+func (t *Type) EncodeRLP(w io.Writer) error { return t.val.EncodeRLP(w) }
+
+// DecodeRLP implements the [rlp.Decoder] interface.
+func (t *Type) DecodeRLP(s *rlp.Stream) error { return t.val.DecodeRLP(s) }
+
var _ = []interface {
json.Marshaler
json.Unmarshaler
@@ -148,15 +172,28 @@ var _ = []interface {
(*concrete[struct{}])(nil),
}
+var _ = []interface {
+ rlp.Encoder
+ rlp.Decoder
+}{
+ (*Type)(nil),
+ (*concrete[struct{}])(nil),
+}
+
// A value is a non-generic wrapper around a [concrete] struct.
type value interface {
get() any
+ isZero() bool
+ equal(*Type) bool
canSetTo(any) bool
set(any) error
mustSet(any)
json.Marshaler
json.Unmarshaler
+ rlp.Encoder
+ rlp.Decoder
+ fmt.Formatter
}
type concrete[T any] struct {
@@ -210,3 +247,5 @@ func (c *concrete[T]) UnmarshalJSON(b []byte) error {
c.val = v
return nil
}
+
+func (c *concrete[T]) EncodeRLP(w io.Writer) error { return rlp.Encode(w, c.val) }
diff --git a/libevm/pseudo/type_test.go b/libevm/pseudo/type_test.go
index d68348cff06e..2413ae421edb 100644
--- a/libevm/pseudo/type_test.go
+++ b/libevm/pseudo/type_test.go
@@ -13,6 +13,7 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// .
+
package pseudo
import (
@@ -116,3 +117,58 @@ func TestPointer(t *testing.T) {
assert.Equal(t, 314159, val.Get().payload, "after setting via pointer")
})
}
+
+func TestIsZero(t *testing.T) {
+ tests := []struct {
+ typ *Type
+ want bool
+ }{
+ {From(0).Type, true},
+ {From(1).Type, false},
+ {From("").Type, true},
+ {From("x").Type, false},
+ {From((*testing.T)(nil)).Type, true},
+ {From(t).Type, false},
+ {From(false).Type, true},
+ {From(true).Type, false},
+ }
+
+ for _, tt := range tests {
+ assert.Equalf(t, tt.want, tt.typ.IsZero(), "%T(%[1]v) IsZero()", tt.typ.Interface())
+ }
+}
+
+type isEqualStub struct {
+ isEqual bool
+}
+
+var _ EqualityChecker[isEqualStub] = (*isEqualStub)(nil)
+
+func (s isEqualStub) Equal(isEqualStub) bool {
+ return s.isEqual
+}
+
+func TestEqual(t *testing.T) {
+ isEqual := isEqualStub{true}
+ notEqual := isEqualStub{false}
+
+ tests := []struct {
+ a, b *Type
+ want bool
+ }{
+ {From(42).Type, From(42).Type, true},
+ {From(99).Type, From("").Type, false},
+ {From(false).Type, From("").Type, false}, // sorry JavaScript, you're wrong
+ {From(isEqual).Type, From(isEqual).Type, true},
+ {From(notEqual).Type, From(notEqual).Type, false},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ t.Logf("a = %+v", tt.a)
+ t.Logf("b = %+v", tt.b)
+ assert.Equal(t, tt.want, tt.a.Equal(tt.b), "a.Equals(b)")
+ assert.Equal(t, tt.want, tt.b.Equal(tt.a), "b.Equals(a)")
+ })
+ }
+}
diff --git a/libevm/testonly/testonly.go b/libevm/testonly/testonly.go
new file mode 100644
index 000000000000..74a9a81d6f30
--- /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 0e9981bcc32e..c24f47537fb4 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