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}"); 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/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 new file mode 100644 index 00000000000..45f5a940ea0 --- /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 00000000000..8d72bd6de1f --- /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 21eb31e2aa2..3f5e4f26677 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,27 @@ 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 } type concrete[T any] struct { @@ -210,3 +246,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 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)") + }) + } +}