From 854fa2d3b8ce2cd19d4558ddd3d08c5bfa51acc3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 27 Sep 2024 15:50:54 +0100 Subject: [PATCH 01/15] feat: `types.StateAccount` pseudo-generic payload --- core/types/gen_account_rlp.go | 6 ++ core/types/rlp_payload.libevm.go | 39 ++++++++++++ core/types/state_account.go | 2 + core/types/state_account.libevm_test.go | 83 +++++++++++++++++++++++++ libevm/pseudo/type.go | 24 +++++++ 5 files changed, 154 insertions(+) create mode 100644 core/types/rlp_payload.libevm.go create mode 100644 core/types/state_account.libevm_test.go diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go index 8b424493afb8..0ffbab51408a 100644 --- a/core/types/gen_account_rlp.go +++ b/core/types/gen_account_rlp.go @@ -16,6 +16,12 @@ func (obj *StateAccount) EncodeRLP(_w io.Writer) error { } w.WriteBytes(obj.Root[:]) w.WriteBytes(obj.CodeHash) + _tmp1 := obj.Extra != nil + if _tmp1 { + 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..ec5f9b576399 --- /dev/null +++ b/core/types/rlp_payload.libevm.go @@ -0,0 +1,39 @@ +package types + +import ( + "io" + + "github.com/ethereum/go-ethereum/libevm/pseudo" + "github.com/ethereum/go-ethereum/rlp" +) + +type RLPPayload struct { + t *pseudo.Type +} + +func NewRLPPayload[T any]() (*RLPPayload, *pseudo.Value[T]) { + var x T + return RLPPayloadOf(x) +} + +func RLPPayloadOf[T any](x T) (*RLPPayload, *pseudo.Value[T]) { + p := pseudo.From(x) + return &RLPPayload{p.Type}, p.Value +} + +var _ interface { + rlp.Encoder + rlp.Decoder +} = (*RLPPayload)(nil) + +func (p *RLPPayload) EncodeRLP(w io.Writer) error { + if p == nil || p.t == nil { + return nil + } + return p.t.EncodeRLP(w) +} + +func (p *RLPPayload) DecodeRLP(s *rlp.Stream) error { + // DO NOT MERGE without implementation + return nil +} diff --git a/core/types/state_account.go b/core/types/state_account.go index 52ef843b3527..8684639dda39 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -33,6 +33,8 @@ type StateAccount struct { Balance *uint256.Int Root common.Hash // merkle root of the storage trie CodeHash []byte + + Extra *RLPPayload `rlp:"optional"` } // NewEmptyStateAccount constructs an empty state account. diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go new file mode 100644 index 000000000000..f8704bd2c529 --- /dev/null +++ b/core/types/state_account.libevm_test.go @@ -0,0 +1,83 @@ +package types + +import ( + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +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. + tests := []struct { + name string + acc *StateAccount + wantHex string + }{ + { + 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`, + }, + { + name: "true boolean extra", + acc: &StateAccount{ + Nonce: 0x444444, + Balance: uint256.NewInt(0x666666), + Root: common.Hash{}, + CodeHash: []byte{0xbb, 0xbb, 0xbb}, + Extra: func() *RLPPayload { + p, _ := RLPPayloadOf(true) // not an error being dropped + return p + }(), + }, + wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb01`, + }, + { + name: "false boolean extra", + acc: &StateAccount{ + Nonce: 0x444444, + Balance: uint256.NewInt(0x666666), + Root: common.Hash{}, + CodeHash: []byte{0xbb, 0xbb, 0xbb}, + Extra: func() *RLPPayload { + p, _ := RLPPayloadOf(false) + return p + }(), + }, + wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb80`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := rlp.EncodeToBytes(tt.acc) + require.NoError(t, err) + t.Logf("got: %#x", got) + + tt.wantHex = strings.TrimPrefix(tt.wantHex, "0x") + require.Equal(t, common.Hex2Bytes(tt.wantHex), got) + }) + } +} diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index 21eb31e2aa2f..3f0ad1bc04b2 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 @@ -139,6 +142,10 @@ 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) } +func (t *Type) EncodeRLP(w io.Writer) error { return t.val.EncodeRLP(w) } + +func (t *Type) DecodeRLP(s *rlp.Stream) error { return t.val.DecodeRLP(s) } + var _ = []interface { json.Marshaler json.Unmarshaler @@ -148,6 +155,14 @@ 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 @@ -157,6 +172,8 @@ type value interface { json.Marshaler json.Unmarshaler + rlp.Encoder + rlp.Decoder } type concrete[T any] struct { @@ -210,3 +227,10 @@ 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) } + +func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error { + s.Kind() // is this required? + return s.Decode(c.val) +} From 10eb6f88a7baa1092808c212c845f380e5628b70 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 30 Sep 2024 21:08:28 +0100 Subject: [PATCH 02/15] feat: registration of `StateAccount` payload type --- core/types/gen_account_rlp.go | 7 +- core/types/rlp_payload.libevm.go | 37 ++++++---- core/types/state_account.go | 2 +- core/types/state_account.libevm_test.go | 92 ++++++++++++++++--------- 4 files changed, 89 insertions(+), 49 deletions(-) diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go index 0ffbab51408a..9205318512eb 100644 --- a/core/types/gen_account_rlp.go +++ b/core/types/gen_account_rlp.go @@ -16,11 +16,8 @@ func (obj *StateAccount) EncodeRLP(_w io.Writer) error { } w.WriteBytes(obj.Root[:]) w.WriteBytes(obj.CodeHash) - _tmp1 := obj.Extra != nil - if _tmp1 { - if err := obj.Extra.EncodeRLP(w); err != nil { - return err - } + 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 index ec5f9b576399..9d99f6367088 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -7,33 +7,46 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -type RLPPayload struct { - t *pseudo.Type +type Extras[SA any] struct{} + +func RegisterExtras[SA any](extras Extras[SA]) { + if registeredExtras != nil { + panic("re-registration of Extras") + } + registeredExtras = &extraConstructors{ + newStateAccount: pseudo.NewConstructor[SA]().Zero, + } } -func NewRLPPayload[T any]() (*RLPPayload, *pseudo.Value[T]) { - var x T - return RLPPayloadOf(x) +var registeredExtras *extraConstructors + +type extraConstructors struct { + newStateAccount func() *pseudo.Type } -func RLPPayloadOf[T any](x T) (*RLPPayload, *pseudo.Value[T]) { - p := pseudo.From(x) - return &RLPPayload{p.Type}, p.Value +type StateAccountExtra struct { + t *pseudo.Type } var _ interface { rlp.Encoder rlp.Decoder -} = (*RLPPayload)(nil) +} = (*StateAccountExtra)(nil) -func (p *RLPPayload) EncodeRLP(w io.Writer) error { - if p == nil || p.t == nil { +func (p *StateAccountExtra) EncodeRLP(w io.Writer) error { + switch r := registeredExtras; { + case r == nil: return nil + case p == nil: + p = &StateAccountExtra{} + fallthrough + case p.t == nil: + p.t = r.newStateAccount() } return p.t.EncodeRLP(w) } -func (p *RLPPayload) DecodeRLP(s *rlp.Stream) error { +func (p *StateAccountExtra) DecodeRLP(s *rlp.Stream) error { // DO NOT MERGE without implementation return nil } diff --git a/core/types/state_account.go b/core/types/state_account.go index 8684639dda39..b41c2c7eb5fa 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -34,7 +34,7 @@ type StateAccount struct { Root common.Hash // merkle root of the storage trie CodeHash []byte - Extra *RLPPayload `rlp:"optional"` + Extra *StateAccountExtra } // NewEmptyStateAccount constructs an empty state account. diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go index f8704bd2c529..bf61b1460343 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/libevm/pseudo" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -15,63 +16,92 @@ func TestStateAccountRLP(t *testing.T) { // 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. - tests := []struct { - name string - acc *StateAccount - wantHex string - }{ - { - name: "vanilla geth account", - acc: &StateAccount{ - Nonce: 0xcccccc, - Balance: uint256.NewInt(0x555555), - Root: common.MaxHash, - CodeHash: []byte{0x77, 0x77, 0x77}, + + type test struct { + name string + register func() + acc *StateAccount + wantHex string + } + + explicitFalseBoolean := test{ + name: "explicit false-boolean extra", + register: func() { + RegisterExtras(Extras[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: `0xed83cccccc83555555a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83777777`, }, + 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: "vanilla geth account", + name: "true-boolean extra", + register: func() { + RegisterExtras(Extras[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: `0xed8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb`, + wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb01`, }, { - name: "true boolean extra", + name: "vanilla geth account", acc: &StateAccount{ - Nonce: 0x444444, - Balance: uint256.NewInt(0x666666), - Root: common.Hash{}, - CodeHash: []byte{0xbb, 0xbb, 0xbb}, - Extra: func() *RLPPayload { - p, _ := RLPPayloadOf(true) // not an error being dropped - return p - }(), + Nonce: 0xcccccc, + Balance: uint256.NewInt(0x555555), + Root: common.MaxHash, + CodeHash: []byte{0x77, 0x77, 0x77}, }, - wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb01`, + wantHex: `0xed83cccccc83555555a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83777777`, }, { - name: "false boolean extra", + name: "vanilla geth account", acc: &StateAccount{ Nonce: 0x444444, Balance: uint256.NewInt(0x666666), Root: common.Hash{}, CodeHash: []byte{0xbb, 0xbb, 0xbb}, - Extra: func() *RLPPayload { - p, _ := RLPPayloadOf(false) - return p - }(), }, - wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb80`, + 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 + }) + } + got, err := rlp.EncodeToBytes(tt.acc) require.NoError(t, err) t.Logf("got: %#x", got) From bb4c91422440302328d3ca72858bd0626fff9ab9 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 09:19:44 +0100 Subject: [PATCH 03/15] chore: mark `eth/tracers/logger` flaky --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1282b281d637..542e16247d3b 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/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}"); From 095415f8f9eba147737da58c15b841a58767a5eb Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 09:23:20 +0100 Subject: [PATCH 04/15] chore: copyright header + `gci` --- core/types/rlp_payload.libevm.go | 16 ++++++++++++++++ core/types/state_account.libevm_test.go | 21 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index 9d99f6367088..85f2a6c49cf3 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -1,3 +1,19 @@ +// 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 ( diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go index bf61b1460343..895c5247e56a 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -1,14 +1,31 @@ +// 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/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" - "github.com/holiman/uint256" - "github.com/stretchr/testify/require" ) func TestStateAccountRLP(t *testing.T) { From 0db95284b32bfa9978bb798810389f05783ce904 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 11:29:14 +0100 Subject: [PATCH 05/15] test: lock default `types.SlimAccount` RLP encoding --- core/types/state_account.libevm_test.go | 83 +++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go index 895c5247e56a..8261e286b7dc 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -20,6 +20,8 @@ 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" @@ -118,13 +120,84 @@ func TestStateAccountRLP(t *testing.T) { registeredExtras = nil }) } + assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex) + }) + } +} + +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`, + }, + } - got, err := rlp.EncodeToBytes(tt.acc) - require.NoError(t, err) - t.Logf("got: %#x", got) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex) - tt.wantHex = strings.TrimPrefix(tt.wantHex, "0x") - require.Equal(t, common.Hex2Bytes(tt.wantHex), got) + var got SlimAccount + require.NoError(t, rlp.DecodeBytes(buf, &got), "rlp.DecodeBytes()") + + // The require package differentiates between empty and nil slices + // and doesn't have a configuration mechanism. + if diff := cmp.Diff(tt.acc, got, cmpopts.EquateEmpty()); 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 +} From cda5afb908f42c048a4ae8978e608309bb9ecc87 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 12:00:28 +0100 Subject: [PATCH 06/15] feat: `vm.SlimAccount.Extra` from `StateAccount` equiv --- core/state/snapshot/snapshot_test.go | 8 ++--- core/types/gen_slim_account_rlp.libevm.go | 24 +++++++++++++++ core/types/rlp_payload.libevm.go | 36 +++++++++++++++++------ core/types/state_account.go | 8 ++++- core/types/state_account.libevm_test.go | 32 +++++++++++++------- 5 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 core/types/gen_slim_account_rlp.libevm.go diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go index a9ab3eaea379..063ff3a53c35 100644 --- a/core/state/snapshot/snapshot_test.go +++ b/core/state/snapshot/snapshot_test.go @@ -119,7 +119,7 @@ func TestDiskLayerExternalInvalidationFullFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -169,7 +169,7 @@ func TestDiskLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -231,7 +231,7 @@ func TestDiffLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the accumulator diff layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) + t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -280,7 +280,7 @@ func TestPostCapBasicDataAccess(t *testing.T) { // shouldErr checks that an account access errors as expected shouldErr := func(layer *diffLayer, key string) error { if data, err := layer.Account(common.HexToHash(key)); err == nil { - return fmt.Errorf("expected error, got data %x", data) + return fmt.Errorf("expected error, got data %+v", data) } return nil } 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 index 85f2a6c49cf3..c4dfb6801ef4 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -30,39 +30,57 @@ func RegisterExtras[SA any](extras Extras[SA]) { panic("re-registration of Extras") } registeredExtras = &extraConstructors{ - newStateAccount: pseudo.NewConstructor[SA]().Zero, + newStateAccount: pseudo.NewConstructor[SA]().Zero, + cloneStateAccount: extras.cloneStateAccount, + } +} + +func (e Extras[SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra { + v := pseudo.MustNewValue[SA](s.t) + return &StateAccountExtra{ + t: pseudo.From(v.Get()).Type, } } var registeredExtras *extraConstructors type extraConstructors struct { - newStateAccount func() *pseudo.Type + newStateAccount func() *pseudo.Type + cloneStateAccount func(*StateAccountExtra) *StateAccountExtra } type StateAccountExtra struct { t *pseudo.Type } +func (e *StateAccountExtra) clone() *StateAccountExtra { + switch r := registeredExtras; { + case r == nil, e == nil: + return nil + default: + return r.cloneStateAccount(e) + } +} + var _ interface { rlp.Encoder rlp.Decoder } = (*StateAccountExtra)(nil) -func (p *StateAccountExtra) EncodeRLP(w io.Writer) error { +func (e *StateAccountExtra) EncodeRLP(w io.Writer) error { switch r := registeredExtras; { case r == nil: return nil - case p == nil: - p = &StateAccountExtra{} + case e == nil: + e = &StateAccountExtra{} fallthrough - case p.t == nil: - p.t = r.newStateAccount() + case e.t == nil: + e.t = r.newStateAccount() } - return p.t.EncodeRLP(w) + return e.t.EncodeRLP(w) } -func (p *StateAccountExtra) DecodeRLP(s *rlp.Stream) error { +func (e *StateAccountExtra) DecodeRLP(s *rlp.Stream) error { // DO NOT MERGE without implementation return nil } diff --git a/core/types/state_account.go b/core/types/state_account.go index b41c2c7eb5fa..608c3697357f 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. @@ -57,6 +58,7 @@ func (acct *StateAccount) Copy() *StateAccount { Balance: balance, Root: acct.Root, CodeHash: common.CopyBytes(acct.CodeHash), + Extra: acct.Extra.clone(), } } @@ -68,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. @@ -75,6 +79,7 @@ func SlimAccountRLP(account StateAccount) []byte { slim := SlimAccount{ Nonce: account.Nonce, Balance: account.Balance, + Extra: account.Extra.clone(), } if account.Root != EmptyRootHash { slim.Root = account.Root[:] @@ -82,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) } @@ -98,6 +103,7 @@ func FullAccount(data []byte) (*StateAccount, error) { } var account StateAccount account.Nonce, account.Balance = slim.Nonce, slim.Balance + account.Extra = slim.Extra.clone() // 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 index 8261e286b7dc..0654122823a5 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -142,18 +142,18 @@ func TestSlimAccountRLP(t *testing.T) { // modifications, to lock in default behaviour. tests := []struct { name string - acc SlimAccount + acc *SlimAccount wantHex string }{ { - acc: SlimAccount{ + acc: &SlimAccount{ Nonce: 0x444444, Balance: uint256.NewInt(0x777777), }, wantHex: `0xca83444444837777778080`, }, { - acc: SlimAccount{ + acc: &SlimAccount{ Nonce: 0x444444, Balance: uint256.NewInt(0x777777), Root: common.MaxHash[:], @@ -161,7 +161,7 @@ func TestSlimAccountRLP(t *testing.T) { wantHex: `0xea8344444483777777a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80`, }, { - acc: SlimAccount{ + acc: &SlimAccount{ Nonce: 0x444444, Balance: uint256.NewInt(0x777777), CodeHash: common.MaxHash[:], @@ -169,7 +169,7 @@ func TestSlimAccountRLP(t *testing.T) { wantHex: `0xea834444448377777780a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, }, { - acc: SlimAccount{ + acc: &SlimAccount{ Nonce: 0x444444, Balance: uint256.NewInt(0x777777), Root: common.MaxHash[:], @@ -183,12 +183,24 @@ func TestSlimAccountRLP(t *testing.T) { t.Run(tt.name, func(t *testing.T) { buf := assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex) - var got SlimAccount - require.NoError(t, rlp.DecodeBytes(buf, &got), "rlp.DecodeBytes()") + 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(), + cmp.Comparer(func(a, b *StateAccountExtra) bool { + aNil := a == nil || a.t == nil + bNil := b == nil || b.t == nil + if aNil && bNil { + return true + } + return false // DO NOT MERGE + }), + } - // The require package differentiates between empty and nil slices - // and doesn't have a configuration mechanism. - if diff := cmp.Diff(tt.acc, got, cmpopts.EquateEmpty()); diff != "" { + 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) } }) From 780a59216b5244967dbabf8753e3276451bbc73f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 12:03:21 +0100 Subject: [PATCH 07/15] chore: placate the linter --- libevm/pseudo/type.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index 3f0ad1bc04b2..a786153d4ad6 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -231,6 +231,9 @@ func (c *concrete[T]) UnmarshalJSON(b []byte) error { func (c *concrete[T]) EncodeRLP(w io.Writer) error { return rlp.Encode(w, c.val) } func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error { - s.Kind() // is this required? + if _, _, err := s.Kind(); err != nil { + // DO NOT MERGE: is calling Kind() a necessary step? + return err + } return s.Decode(c.val) } From 99255901c591ce46fafa8691b7454366ba3b2bc0 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 12:38:25 +0100 Subject: [PATCH 08/15] test: `pseudo.Type.EncodeRLP()` --- libevm/ethtest/rand.go | 32 ++++++++++++++++-- libevm/pseudo/rlp_test.go | 68 +++++++++++++++++++++++++++++++++++++++ libevm/pseudo/type.go | 2 ++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 libevm/pseudo/rlp_test.go 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/rlp_test.go b/libevm/pseudo/rlp_test.go new file mode 100644 index 000000000000..b4c6222684d1 --- /dev/null +++ b/libevm/pseudo/rlp_test.go @@ -0,0 +1,68 @@ +// 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++ { + rng := ethtest.NewPseudoRand(seed) + + t.Run("fuzz", func(t *testing.T) { + t.Parallel() + + 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 + got, err := rlp.EncodeToBytes(typ) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", typ) + + require.Equalf(t, want, got, "RLP encoding of %T (canonical) vs %T (under test)", hdr, typ) + }) + } +} diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index a786153d4ad6..080e0e1d5e51 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -142,8 +142,10 @@ 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 { From 0d19b340ab9c1b03044545083ea8a44461eb4ee1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 13:32:20 +0100 Subject: [PATCH 09/15] test: `pseudo.Type.DecodeRLP()` --- libevm/pseudo/reflect.go | 32 ++++++++++++++++++++++++++++++++ libevm/pseudo/rlp_test.go | 10 ++++++++-- libevm/pseudo/type.go | 5 +---- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 libevm/pseudo/reflect.go diff --git a/libevm/pseudo/reflect.go b/libevm/pseudo/reflect.go new file mode 100644 index 000000000000..9b995dd481b6 --- /dev/null +++ b/libevm/pseudo/reflect.go @@ -0,0 +1,32 @@ +// 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" + +// 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]) ensureNonNilPointer() { + v := reflect.ValueOf(c.val) + if v.Kind() != reflect.Pointer || !v.IsNil() { + return + } + 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 +} diff --git a/libevm/pseudo/rlp_test.go b/libevm/pseudo/rlp_test.go index b4c6222684d1..abe2f8a0e6ef 100644 --- a/libevm/pseudo/rlp_test.go +++ b/libevm/pseudo/rlp_test.go @@ -59,10 +59,16 @@ func TestRLPEquivalence(t *testing.T) { require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", hdr) typ := pseudo.From(hdr).Type - got, err := rlp.EncodeToBytes(typ) + gotRLP, err := rlp.EncodeToBytes(typ) require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", typ) - require.Equalf(t, want, got, "RLP encoding of %T (canonical) vs %T (under test)", hdr, 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.NoError(t, rlp.DecodeBytes(gotRLP, pseudo.Type)) + require.Equal(t, hdr, pseudo.Value.Get()) + }) }) } } diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index 080e0e1d5e51..e0f8c9fd6f23 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -233,9 +233,6 @@ func (c *concrete[T]) UnmarshalJSON(b []byte) error { func (c *concrete[T]) EncodeRLP(w io.Writer) error { return rlp.Encode(w, c.val) } func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error { - if _, _, err := s.Kind(); err != nil { - // DO NOT MERGE: is calling Kind() a necessary step? - return err - } + c.ensureNonNilPointer() return s.Decode(c.val) } From 782f028cdc329040adac4e5e57e2f8cff545eee7 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 14:06:52 +0100 Subject: [PATCH 10/15] fix: `pseudo.Type.DecodeRLP()` with non-pointer type --- libevm/pseudo/constructor.go | 1 + libevm/pseudo/reflect.go | 22 +++++++++++++++------- libevm/pseudo/rlp_test.go | 20 ++++++++++++++++---- libevm/pseudo/type.go | 5 ----- 4 files changed, 32 insertions(+), 16 deletions(-) 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/reflect.go b/libevm/pseudo/reflect.go index 9b995dd481b6..1417a76878e2 100644 --- a/libevm/pseudo/reflect.go +++ b/libevm/pseudo/reflect.go @@ -16,17 +16,25 @@ package pseudo -import "reflect" +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]) ensureNonNilPointer() { - v := reflect.ValueOf(c.val) - if v.Kind() != reflect.Pointer || !v.IsNil() { - return +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) } - 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 } diff --git a/libevm/pseudo/rlp_test.go b/libevm/pseudo/rlp_test.go index abe2f8a0e6ef..8d72bd6de1ff 100644 --- a/libevm/pseudo/rlp_test.go +++ b/libevm/pseudo/rlp_test.go @@ -32,10 +32,11 @@ func TestRLPEquivalence(t *testing.T) { t.Parallel() for seed := uint64(0); seed < 20; seed++ { - rng := ethtest.NewPseudoRand(seed) + seed := seed - t.Run("fuzz", func(t *testing.T) { + t.Run("fuzz pointer-type round trip", func(t *testing.T) { t.Parallel() + rng := ethtest.NewPseudoRand(seed) hdr := &types.Header{ ParentHash: rng.Hash(), @@ -66,9 +67,20 @@ func TestRLPEquivalence(t *testing.T) { t.Run("decode", func(t *testing.T) { pseudo := pseudo.Zero[*types.Header]() - require.NoError(t, rlp.DecodeBytes(gotRLP, pseudo.Type)) - require.Equal(t, hdr, pseudo.Value.Get()) + 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 e0f8c9fd6f23..42694a771f47 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -231,8 +231,3 @@ func (c *concrete[T]) UnmarshalJSON(b []byte) error { } func (c *concrete[T]) EncodeRLP(w io.Writer) error { return rlp.Encode(w, c.val) } - -func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error { - c.ensureNonNilPointer() - return s.Decode(c.val) -} From a981d546eb72b55a22b8f80ee96b48f26ef7045e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 15:29:10 +0100 Subject: [PATCH 11/15] feat: `pseudo.Type.IsZero()` and `Type.Equal(*Type)` --- libevm/pseudo/reflect.go | 19 +++++++++++++ libevm/pseudo/type.go | 17 ++++++++++++ libevm/pseudo/type_test.go | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/libevm/pseudo/reflect.go b/libevm/pseudo/reflect.go index 1417a76878e2..260f8604756a 100644 --- a/libevm/pseudo/reflect.go +++ b/libevm/pseudo/reflect.go @@ -26,6 +26,25 @@ import ( // 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: + 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: diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index 42694a771f47..3f5e4f26677a 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -124,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 @@ -168,6 +183,8 @@ var _ = []interface { // 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) 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)") + }) + } +} From 1602fdb31488c3aa3f6e128980029097cbb982d9 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 15:31:49 +0100 Subject: [PATCH 12/15] feat: `types.StateAccountExtra.DecodeRLP()` --- core/state/statedb.go | 1 + core/types/rlp_payload.libevm.go | 11 +++++++++-- core/types/state_account.libevm_test.go | 26 +++++++++++++++++-------- libevm/pseudo/reflect.go | 1 + 4 files changed, 29 insertions(+), 10 deletions(-) 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/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index c4dfb6801ef4..3ecc304086e2 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -81,6 +81,13 @@ func (e *StateAccountExtra) EncodeRLP(w io.Writer) error { } func (e *StateAccountExtra) DecodeRLP(s *rlp.Stream) error { - // DO NOT MERGE without implementation - return nil + switch r := registeredExtras; { + case r == nil: + return nil + case e.t == nil: + e.t = r.newStateAccount() + fallthrough + default: + return s.Decode(e.t) + } } diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go index 0654122823a5..26865ec5dd04 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -30,6 +30,15 @@ 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 @@ -121,6 +130,15 @@ func TestStateAccountRLP(t *testing.T) { }) } 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) + } + }) }) } } @@ -190,14 +208,6 @@ func TestSlimAccountRLP(t *testing.T) { // The require package differentiates between empty and nil // slices and doesn't have a configuration mechanism. cmpopts.EquateEmpty(), - cmp.Comparer(func(a, b *StateAccountExtra) bool { - aNil := a == nil || a.t == nil - bNil := b == nil || b.t == nil - if aNil && bNil { - return true - } - return false // DO NOT MERGE - }), } if diff := cmp.Diff(tt.acc, got, opts...); diff != "" { diff --git a/libevm/pseudo/reflect.go b/libevm/pseudo/reflect.go index 260f8604756a..45f5a940ea0b 100644 --- a/libevm/pseudo/reflect.go +++ b/libevm/pseudo/reflect.go @@ -41,6 +41,7 @@ func (c *concrete[T]) equal(t *Type) bool { case EqualityChecker[T]: return v.Equal(d.val) default: + // See rationale for reflection in [concrete.isZero]. return reflect.DeepEqual(c.val, d.val) } } From 17022cefbef761e67853916d10bc98af589cc2dd Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 16:42:55 +0100 Subject: [PATCH 13/15] fix: remove unnecessary `StateAccountExtra.clone()` --- core/types/state_account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/types/state_account.go b/core/types/state_account.go index 608c3697357f..3de3d4f64ee0 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -79,7 +79,7 @@ func SlimAccountRLP(account StateAccount) []byte { slim := SlimAccount{ Nonce: account.Nonce, Balance: account.Balance, - Extra: account.Extra.clone(), + Extra: account.Extra, } if account.Root != EmptyRootHash { slim.Root = account.Root[:] @@ -103,7 +103,7 @@ func FullAccount(data []byte) (*StateAccount, error) { } var account StateAccount account.Nonce, account.Balance = slim.Nonce, slim.Balance - account.Extra = slim.Extra.clone() + account.Extra = slim.Extra // Interpret the storage root and code hash in slim format. if len(slim.Root) == 0 { From f1a1c672408ee4e4cbc41994975e825855ccd6c2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 19:39:21 +0100 Subject: [PATCH 14/15] refactor: readability --- core/types/rlp_payload.libevm.go | 90 ++++++++++++++++++++----- core/types/state_account.libevm_test.go | 4 +- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index 3ecc304086e2..c3578fe3e79f 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -23,23 +23,27 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -type Extras[SA any] struct{} - -func RegisterExtras[SA any](extras Extras[SA]) { +// 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{ newStateAccount: pseudo.NewConstructor[SA]().Zero, - cloneStateAccount: extras.cloneStateAccount, - } -} - -func (e Extras[SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra { - v := pseudo.MustNewValue[SA](s.t) - return &StateAccountExtra{ - t: pseudo.From(v.Get()).Type, + cloneStateAccount: extra.cloneStateAccount, } + return extra } var registeredExtras *extraConstructors @@ -49,10 +53,6 @@ type extraConstructors struct { cloneStateAccount func(*StateAccountExtra) *StateAccountExtra } -type StateAccountExtra struct { - t *pseudo.Type -} - func (e *StateAccountExtra) clone() *StateAccountExtra { switch r := registeredExtras; { case r == nil, e == nil: @@ -62,11 +62,70 @@ func (e *StateAccountExtra) clone() *StateAccountExtra { } } +// 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 } = (*StateAccountExtra)(nil) +// EncodeRLP implements the [rlp.Encoder] interface. func (e *StateAccountExtra) EncodeRLP(w io.Writer) error { switch r := registeredExtras; { case r == nil: @@ -80,6 +139,7 @@ func (e *StateAccountExtra) EncodeRLP(w io.Writer) error { 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: diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go index 26865ec5dd04..fa5810ed84eb 100644 --- a/core/types/state_account.libevm_test.go +++ b/core/types/state_account.libevm_test.go @@ -55,7 +55,7 @@ func TestStateAccountRLP(t *testing.T) { explicitFalseBoolean := test{ name: "explicit false-boolean extra", register: func() { - RegisterExtras(Extras[bool]{}) + RegisterExtras[bool]() }, acc: &StateAccount{ Nonce: 0x444444, @@ -85,7 +85,7 @@ func TestStateAccountRLP(t *testing.T) { { name: "true-boolean extra", register: func() { - RegisterExtras(Extras[bool]{}) + RegisterExtras[bool]() }, acc: &StateAccount{ Nonce: 0x444444, From e2af2a6b9c86674a58f4f0df98ca42103e7c2180 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 2 Oct 2024 12:22:01 +0100 Subject: [PATCH 15/15] feat: `pseudo.Type.Format()` implements `fmt.Formatter` --- core/state/snapshot/snapshot_test.go | 8 +-- core/types/rlp_payload.libevm.go | 22 ++++++++ libevm/pseudo/fmt.go | 56 +++++++++++++++++++ libevm/pseudo/fmt_test.go | 82 ++++++++++++++++++++++++++++ libevm/pseudo/type.go | 1 + 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 libevm/pseudo/fmt.go create mode 100644 libevm/pseudo/fmt_test.go diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go index 063ff3a53c35..a9ab3eaea379 100644 --- a/core/state/snapshot/snapshot_test.go +++ b/core/state/snapshot/snapshot_test.go @@ -119,7 +119,7 @@ func TestDiskLayerExternalInvalidationFullFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -169,7 +169,7 @@ func TestDiskLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the base layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -231,7 +231,7 @@ func TestDiffLayerExternalInvalidationPartialFlatten(t *testing.T) { } // Since the accumulator diff layer was modified, ensure that data retrievals on the external reference fail if acc, err := ref.Account(common.HexToHash("0x01")); err != ErrSnapshotStale { - t.Errorf("stale reference returned account: %+v (err: %v)", acc, err) + t.Errorf("stale reference returned account: %#x (err: %v)", acc, err) } if slot, err := ref.Storage(common.HexToHash("0xa1"), common.HexToHash("0xb1")); err != ErrSnapshotStale { t.Errorf("stale reference returned storage slot: %#x (err: %v)", slot, err) @@ -280,7 +280,7 @@ func TestPostCapBasicDataAccess(t *testing.T) { // shouldErr checks that an account access errors as expected shouldErr := func(layer *diffLayer, key string) error { if data, err := layer.Account(common.HexToHash(key)); err == nil { - return fmt.Errorf("expected error, got data %+v", data) + return fmt.Errorf("expected error, got data %x", data) } return nil } diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index c3578fe3e79f..04b10d295cfd 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -17,6 +17,7 @@ package types import ( + "fmt" "io" "github.com/ethereum/go-ethereum/libevm/pseudo" @@ -40,6 +41,10 @@ func RegisterExtras[SA any]() ExtraPayloads[SA] { } 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, } @@ -49,6 +54,7 @@ func RegisterExtras[SA any]() ExtraPayloads[SA] { var registeredExtras *extraConstructors type extraConstructors struct { + stateAccountType string newStateAccount func() *pseudo.Type cloneStateAccount func(*StateAccountExtra) *StateAccountExtra } @@ -123,6 +129,7 @@ func (e *StateAccountExtra) payload() *pseudo.Type { var _ interface { rlp.Encoder rlp.Decoder + fmt.Formatter } = (*StateAccountExtra)(nil) // EncodeRLP implements the [rlp.Encoder] interface. @@ -151,3 +158,18 @@ func (e *StateAccountExtra) DecodeRLP(s *rlp.Stream) error { 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/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 {