From f0ae9c50eb3efb5d33b65dfd3ff1b8e47f0ccfc5 Mon Sep 17 00:00:00 2001
From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com>
Date: Wed, 2 Oct 2024 16:00:13 +0100
Subject: [PATCH] feat: `types.StateAccount` pseudo-generic payload (#44)
Some of the changes in the full commit history were merged into `libevm` as part of #43 in `336a289` and then merged back into this branch as `5b15698`. Cherry-picking commits was not possible as some touched both halves of the changes; the squash-merges will, however, make this convoluted history irrelevant.
* feat: `types.StateAccount` pseudo-generic payload
* feat: registration of `StateAccount` payload type
* chore: mark `eth/tracers/logger` flaky
* chore: copyright header + `gci`
* test: lock default `types.SlimAccount` RLP encoding
* feat: `vm.SlimAccount.Extra` from `StateAccount` equiv
* chore: placate the linter
* test: `pseudo.Type.EncodeRLP()`
* test: `pseudo.Type.DecodeRLP()`
* fix: `pseudo.Type.DecodeRLP()` with non-pointer type
* feat: `pseudo.Type.IsZero()` and `Type.Equal(*Type)`
* feat: `types.StateAccountExtra.DecodeRLP()`
* fix: remove unnecessary `StateAccountExtra.clone()`
* refactor: readability
* feat: `pseudo.Type.Format()` implements `fmt.Formatter`
---
core/state/statedb.go | 1 +
core/types/gen_account_rlp.go | 3 +
core/types/gen_slim_account_rlp.libevm.go | 24 +++
core/types/rlp_payload.libevm.go | 175 +++++++++++++++++
core/types/state_account.go | 10 +-
core/types/state_account.libevm_test.go | 225 ++++++++++++++++++++++
libevm/pseudo/fmt.go | 56 ++++++
libevm/pseudo/fmt_test.go | 82 ++++++++
libevm/pseudo/type.go | 1 +
9 files changed, 576 insertions(+), 1 deletion(-)
create mode 100644 core/types/gen_slim_account_rlp.libevm.go
create mode 100644 core/types/rlp_payload.libevm.go
create mode 100644 core/types/state_account.libevm_test.go
create mode 100644 libevm/pseudo/fmt.go
create mode 100644 libevm/pseudo/fmt_test.go
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..04b10d295cfd
--- /dev/null
+++ b/core/types/rlp_payload.libevm.go
@@ -0,0 +1,175 @@
+// 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/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
+}
+
+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
+}
+
+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..fa5810ed84eb
--- /dev/null
+++ b/core/types/state_account.libevm_test.go
@@ -0,0 +1,225 @@
+// 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 (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
+ // 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 {
+ registeredExtras = nil
+ tt.register()
+ t.Cleanup(func() {
+ registeredExtras = nil
+ })
+ }
+ 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/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/type.go b/libevm/pseudo/type.go
index 3f5e4f26677a..6b80e18a9674 100644
--- a/libevm/pseudo/type.go
+++ b/libevm/pseudo/type.go
@@ -193,6 +193,7 @@ type value interface {
json.Unmarshaler
rlp.Encoder
rlp.Decoder
+ fmt.Formatter
}
type concrete[T any] struct {