From 854fa2d3b8ce2cd19d4558ddd3d08c5bfa51acc3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 27 Sep 2024 15:50:54 +0100 Subject: [PATCH 01/14] 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 8b424493afb..0ffbab51408 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 00000000000..ec5f9b57639 --- /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 52ef843b352..8684639dda3 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 00000000000..f8704bd2c52 --- /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 21eb31e2aa2..3f0ad1bc04b 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/14] 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 0ffbab51408..9205318512e 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 ec5f9b57639..9d99f636708 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 8684639dda3..b41c2c7eb5f 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 f8704bd2c52..bf61b146034 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/14] 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 1282b281d63..542e16247d3 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/14] 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 9d99f636708..85f2a6c49cf 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 bf61b146034..895c5247e56 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/14] 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 895c5247e56..8261e286b7d 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/14] 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 a9ab3eaea37..063ff3a53c3 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 00000000000..e5d76069a75 --- /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 85f2a6c49cf..c4dfb6801ef 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 b41c2c7eb5f..608c3697357 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 8261e286b7d..0654122823a 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/14] 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 3f0ad1bc04b..a786153d4ad 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/14] 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 8584ce11698..eb7f47fda02 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 00000000000..b4c6222684d --- /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 a786153d4ad..080e0e1d5e5 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/14] 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 00000000000..9b995dd481b --- /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 b4c6222684d..abe2f8a0e6e 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 080e0e1d5e5..e0f8c9fd6f2 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/14] 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 42457278572..d72237494de 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 9b995dd481b..1417a76878e 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 abe2f8a0e6e..8d72bd6de1f 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 e0f8c9fd6f2..42694a771f4 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/14] 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 1417a76878e..260f8604756 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 42694a771f4..3f5e4f26677 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 d68348cff06..2413ae421ed 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/14] 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 a4b8cf93e2d..8092155ce59 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 c4dfb6801ef..3ecc304086e 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 0654122823a..26865ec5dd0 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 260f8604756..45f5a940ea0 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 eb166fa2bbd058fdd27a89df65f6c879043fb706 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 15:45:15 +0100 Subject: [PATCH 13/14] chore: revert non-pseudo-package modifications --- core/state/snapshot/snapshot_test.go | 8 ++++---- core/state/statedb.go | 1 - core/types/gen_account_rlp.go | 3 --- core/types/state_account.go | 10 +--------- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go index 063ff3a53c3..a9ab3eaea37 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/state/statedb.go b/core/state/statedb.go index 8092155ce59..a4b8cf93e2d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -579,7 +579,6 @@ 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 9205318512e..8b424493afb 100644 --- a/core/types/gen_account_rlp.go +++ b/core/types/gen_account_rlp.go @@ -16,9 +16,6 @@ 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/state_account.go b/core/types/state_account.go index 608c3697357..52ef843b352 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -25,7 +25,6 @@ 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. @@ -34,8 +33,6 @@ 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. @@ -58,7 +55,6 @@ func (acct *StateAccount) Copy() *StateAccount { Balance: balance, Root: acct.Root, CodeHash: common.CopyBytes(acct.CodeHash), - Extra: acct.Extra.clone(), } } @@ -70,8 +66,6 @@ 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. @@ -79,7 +73,6 @@ 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[:] @@ -87,7 +80,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) } @@ -103,7 +96,6 @@ 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 { From 807f8f548617403f74bbadfc3187ce2564da511c Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 1 Oct 2024 15:45:59 +0100 Subject: [PATCH 14/14] chore: delete non-pseudo-package additions --- core/types/gen_slim_account_rlp.libevm.go | 24 --- core/types/rlp_payload.libevm.go | 93 --------- core/types/state_account.libevm_test.go | 225 ---------------------- 3 files changed, 342 deletions(-) delete mode 100644 core/types/gen_slim_account_rlp.libevm.go delete mode 100644 core/types/rlp_payload.libevm.go delete mode 100644 core/types/state_account.libevm_test.go diff --git a/core/types/gen_slim_account_rlp.libevm.go b/core/types/gen_slim_account_rlp.libevm.go deleted file mode 100644 index e5d76069a75..00000000000 --- a/core/types/gen_slim_account_rlp.libevm.go +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index 3ecc304086e..00000000000 --- a/core/types/rlp_payload.libevm.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 ( - "io" - - "github.com/ethereum/go-ethereum/libevm/pseudo" - "github.com/ethereum/go-ethereum/rlp" -) - -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, - 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 - 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 (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) -} - -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) - } -} diff --git a/core/types/state_account.libevm_test.go b/core/types/state_account.libevm_test.go deleted file mode 100644 index 26865ec5dd0..00000000000 --- a/core/types/state_account.libevm_test.go +++ /dev/null @@ -1,225 +0,0 @@ -// 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(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: `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(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: `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 -}