feat: pseudo.Type RLP round-tripping (#43)

All commits except the last two constitute PRs #43 and #44. The last two reverted files such that only changes to the `pseudo` and `ethtest` packages remain; once this is merged into the `libevm` branch then `libevm` will be merged into the branch for #44 too. Cherry-picking commits was not possible as some touched both halves of the changes; the squash-merges will, however, make this convoluted history irrelevant.

* feat: `types.StateAccount` pseudo-generic payload

* feat: registration of `StateAccount` payload type

* chore: mark `eth/tracers/logger` flaky

* chore: copyright header + `gci`

* test: lock default `types.SlimAccount` RLP encoding

* feat: `vm.SlimAccount.Extra` from `StateAccount` equiv

* chore: placate the linter

* test: `pseudo.Type.EncodeRLP()`

* test: `pseudo.Type.DecodeRLP()`

* fix: `pseudo.Type.DecodeRLP()` with non-pointer type

* feat: `pseudo.Type.IsZero()` and `Type.Equal(*Type)`

* feat: `types.StateAccountExtra.DecodeRLP()`

* chore: revert non-pseudo-package modifications

* chore: delete non-pseudo-package additions
This commit is contained in:
Arran Schlosberg 2024-10-01 16:23:51 +01:00 committed by GitHub
parent 1478c18b9a
commit 336a289f42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 271 additions and 4 deletions

View file

@ -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}");

View file

@ -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"
@ -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))
}

View file

@ -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

60
libevm/pseudo/reflect.go Normal file
View file

@ -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
libevm/pseudo/rlp_test.go Normal file
View file

@ -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")
})
}
}

View file

@ -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) }

View file

@ -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 (
@ -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)")
})
}
}