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