Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
32 changes: 29 additions & 3 deletions libevm/ethtest/rand.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <http://www.gnu.org/licenses/>.

package ethtest

import (
"math/big"

"github.com/holiman/uint256"
"golang.org/x/exp/rand"

"github.com/ethereum/go-ethereum/common"
Expand All @@ -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
}

Expand All @@ -47,18 +56,35 @@ 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
}

// Big returns [rand.Rand.Uint64] as a [big.Int].
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))
}
1 change: 1 addition & 0 deletions libevm/pseudo/constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <http://www.gnu.org/licenses/>.

package pseudo

// A Constructor returns newly constructed [Type] instances for a pre-registered
Expand Down
60 changes: 60 additions & 0 deletions libevm/pseudo/reflect.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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)
}
}
86 changes: 86 additions & 0 deletions libevm/pseudo/rlp_test.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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")
})
}
}
38 changes: 38 additions & 0 deletions libevm/pseudo/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) }
56 changes: 56 additions & 0 deletions libevm/pseudo/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <http://www.gnu.org/licenses/>.

package pseudo

import (
Expand Down Expand Up @@ -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)")
})
}
}