Skip to content

Commit 336a289

Browse files
authored
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
1 parent 1478c18 commit 336a289

File tree

7 files changed

+271
-4
lines changed

7 files changed

+271
-4
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ jobs:
1818
go-version: 1.21.4
1919
- name: Run tests
2020
run: | # Upstream flakes are race conditions exacerbated by concurrent tests
21-
FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
21+
FLAKY_REGEX='go-ethereum/(eth|eth/tracers/logger|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
2222
go list ./... | grep -P "${FLAKY_REGEX}" | xargs -n 1 go test -short;
2323
go test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}");

libevm/ethtest/rand.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
// You should have received a copy of the GNU Lesser General Public License
1414
// along with the go-ethereum library. If not, see
1515
// <http://www.gnu.org/licenses/>.
16+
1617
package ethtest
1718

1819
import (
1920
"math/big"
2021

22+
"github.com/holiman/uint256"
2123
"golang.org/x/exp/rand"
2224

2325
"github.com/ethereum/go-ethereum/common"
@@ -33,9 +35,16 @@ func NewPseudoRand(seed uint64) *PseudoRand {
3335
return &PseudoRand{rand.New(rand.NewSource(seed))}
3436
}
3537

38+
// Read is equivalent to [rand.Rand.Read] except that it doesn't return an error
39+
// because it is guaranteed to be nil.
40+
func (r *PseudoRand) Read(p []byte) int {
41+
n, _ := r.Rand.Read(p) // Guaranteed nil error
42+
return n
43+
}
44+
3645
// Address returns a pseudorandom address.
3746
func (r *PseudoRand) Address() (a common.Address) {
38-
r.Read(a[:]) //nolint:gosec,errcheck // Guaranteed nil error
47+
r.Read(a[:])
3948
return a
4049
}
4150

@@ -47,18 +56,35 @@ func (r *PseudoRand) AddressPtr() *common.Address {
4756

4857
// Hash returns a pseudorandom hash.
4958
func (r *PseudoRand) Hash() (h common.Hash) {
50-
r.Read(h[:]) //nolint:gosec,errcheck // Guaranteed nil error
59+
r.Read(h[:])
5160
return h
5261
}
5362

63+
// HashPtr returns a pointer to a pseudorandom hash.
64+
func (r *PseudoRand) HashPtr() *common.Hash {
65+
h := r.Hash()
66+
return &h
67+
}
68+
5469
// Bytes returns `n` pseudorandom bytes.
5570
func (r *PseudoRand) Bytes(n uint) []byte {
5671
b := make([]byte, n)
57-
r.Read(b) //nolint:gosec,errcheck // Guaranteed nil error
72+
r.Read(b)
5873
return b
5974
}
6075

6176
// Big returns [rand.Rand.Uint64] as a [big.Int].
6277
func (r *PseudoRand) BigUint64() *big.Int {
6378
return new(big.Int).SetUint64(r.Uint64())
6479
}
80+
81+
// Uint64Ptr returns a pointer to a pseudorandom uint64.
82+
func (r *PseudoRand) Uint64Ptr() *uint64 {
83+
u := r.Uint64()
84+
return &u
85+
}
86+
87+
// Uint256 returns a random 256-bit unsigned int.
88+
func (r *PseudoRand) Uint256() *uint256.Int {
89+
return new(uint256.Int).SetBytes(r.Bytes(32))
90+
}

libevm/pseudo/constructor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// You should have received a copy of the GNU Lesser General Public License
1414
// along with the go-ethereum library. If not, see
1515
// <http://www.gnu.org/licenses/>.
16+
1617
package pseudo
1718

1819
// A Constructor returns newly constructed [Type] instances for a pre-registered

libevm/pseudo/reflect.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2024 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package pseudo
18+
19+
import (
20+
"reflect"
21+
22+
"github.com/ethereum/go-ethereum/rlp"
23+
)
24+
25+
// Reflection is used as a last resort in pseudo types so is limited to this
26+
// file to avoid being seen as the norm. If you are adding to this file, please
27+
// try to achieve the same results with type parameters.
28+
29+
func (c *concrete[T]) isZero() bool {
30+
// The alternative would require that T be comparable, which would bubble up
31+
// and invade the rest of the code base.
32+
return reflect.ValueOf(c.val).IsZero()
33+
}
34+
35+
func (c *concrete[T]) equal(t *Type) bool {
36+
d, ok := t.val.(*concrete[T])
37+
if !ok {
38+
return false
39+
}
40+
switch v := any(c.val).(type) {
41+
case EqualityChecker[T]:
42+
return v.Equal(d.val)
43+
default:
44+
// See rationale for reflection in [concrete.isZero].
45+
return reflect.DeepEqual(c.val, d.val)
46+
}
47+
}
48+
49+
func (c *concrete[T]) DecodeRLP(s *rlp.Stream) error {
50+
switch v := reflect.ValueOf(c.val); v.Kind() {
51+
case reflect.Pointer:
52+
if v.IsNil() {
53+
el := v.Type().Elem()
54+
c.val = reflect.New(el).Interface().(T) //nolint:forcetypeassert // Invariant scoped to the last few lines of code so simple to verify
55+
}
56+
return s.Decode(c.val)
57+
default:
58+
return s.Decode(&c.val)
59+
}
60+
}

libevm/pseudo/rlp_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2024 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package pseudo_test
18+
19+
import (
20+
"math/big"
21+
"testing"
22+
23+
"github.com/stretchr/testify/require"
24+
25+
"github.com/ethereum/go-ethereum/core/types"
26+
"github.com/ethereum/go-ethereum/libevm/ethtest"
27+
"github.com/ethereum/go-ethereum/libevm/pseudo"
28+
"github.com/ethereum/go-ethereum/rlp"
29+
)
30+
31+
func TestRLPEquivalence(t *testing.T) {
32+
t.Parallel()
33+
34+
for seed := uint64(0); seed < 20; seed++ {
35+
seed := seed
36+
37+
t.Run("fuzz pointer-type round trip", func(t *testing.T) {
38+
t.Parallel()
39+
rng := ethtest.NewPseudoRand(seed)
40+
41+
hdr := &types.Header{
42+
ParentHash: rng.Hash(),
43+
UncleHash: rng.Hash(),
44+
Coinbase: rng.Address(),
45+
Root: rng.Hash(),
46+
TxHash: rng.Hash(),
47+
ReceiptHash: rng.Hash(),
48+
Difficulty: big.NewInt(rng.Int63()),
49+
Number: big.NewInt(rng.Int63()),
50+
GasLimit: rng.Uint64(),
51+
GasUsed: rng.Uint64(),
52+
Time: rng.Uint64(),
53+
Extra: rng.Bytes(uint(rng.Uint64n(128))),
54+
MixDigest: rng.Hash(),
55+
}
56+
rng.Read(hdr.Bloom[:])
57+
rng.Read(hdr.Nonce[:])
58+
59+
want, err := rlp.EncodeToBytes(hdr)
60+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", hdr)
61+
62+
typ := pseudo.From(hdr).Type
63+
gotRLP, err := rlp.EncodeToBytes(typ)
64+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", typ)
65+
66+
require.Equalf(t, want, gotRLP, "RLP encoding of %T (canonical) vs %T (under test)", hdr, typ)
67+
68+
t.Run("decode", func(t *testing.T) {
69+
pseudo := pseudo.Zero[*types.Header]()
70+
require.NoErrorf(t, rlp.DecodeBytes(gotRLP, pseudo.Type), "rlp.DecodeBytes(..., %T[%T])", pseudo.Type, hdr)
71+
require.Equal(t, hdr, pseudo.Value.Get(), "RLP-decoded value")
72+
})
73+
})
74+
75+
t.Run("fuzz non-pointer decode", func(t *testing.T) {
76+
rng := ethtest.NewPseudoRand(seed)
77+
x := rng.Uint64()
78+
buf, err := rlp.EncodeToBytes(x)
79+
require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", x)
80+
81+
pseudo := pseudo.Zero[uint64]()
82+
require.NoErrorf(t, rlp.DecodeBytes(buf, pseudo.Type), "rlp.DecodeBytes(..., %T[%T])", pseudo.Type, x)
83+
require.Equal(t, x, pseudo.Value.Get(), "RLP-decoded value")
84+
})
85+
}
86+
}

libevm/pseudo/type.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ package pseudo
3131
import (
3232
"encoding/json"
3333
"fmt"
34+
"io"
35+
36+
"github.com/ethereum/go-ethereum/rlp"
3437
)
3538

3639
// A Type wraps a strongly-typed value without exposing information about its
@@ -121,6 +124,21 @@ func MustNewValue[T any](t *Type) *Value[T] {
121124
return v
122125
}
123126

127+
// IsZero reports whether t carries the the zero value for its type.
128+
func (t *Type) IsZero() bool { return t.val.isZero() }
129+
130+
// An EqualityChecker reports if it is equal to another value of the same type.
131+
type EqualityChecker[T any] interface {
132+
Equal(T) bool
133+
}
134+
135+
// Equal reports whether t carries a value equal to that carried by u. If t and
136+
// u carry different types then Equal returns false. If t and u carry the same
137+
// type and said type implements [EqualityChecker] then Equal propagates the
138+
// value returned by the checker. In all other cases, Equal returns
139+
// [reflect.DeepEqual] performed on the payloads carried by t and u.
140+
func (t *Type) Equal(u *Type) bool { return t.val.equal(u) }
141+
124142
// Get returns the value.
125143
func (v *Value[T]) Get() T { return v.t.val.get().(T) } //nolint:forcetypeassert // invariant
126144

@@ -139,6 +157,12 @@ func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() }
139157
// UnmarshalJSON implements the [json.Unmarshaler] interface.
140158
func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) }
141159

160+
// EncodeRLP implements the [rlp.Encoder] interface.
161+
func (t *Type) EncodeRLP(w io.Writer) error { return t.val.EncodeRLP(w) }
162+
163+
// DecodeRLP implements the [rlp.Decoder] interface.
164+
func (t *Type) DecodeRLP(s *rlp.Stream) error { return t.val.DecodeRLP(s) }
165+
142166
var _ = []interface {
143167
json.Marshaler
144168
json.Unmarshaler
@@ -148,15 +172,27 @@ var _ = []interface {
148172
(*concrete[struct{}])(nil),
149173
}
150174

175+
var _ = []interface {
176+
rlp.Encoder
177+
rlp.Decoder
178+
}{
179+
(*Type)(nil),
180+
(*concrete[struct{}])(nil),
181+
}
182+
151183
// A value is a non-generic wrapper around a [concrete] struct.
152184
type value interface {
153185
get() any
186+
isZero() bool
187+
equal(*Type) bool
154188
canSetTo(any) bool
155189
set(any) error
156190
mustSet(any)
157191

158192
json.Marshaler
159193
json.Unmarshaler
194+
rlp.Encoder
195+
rlp.Decoder
160196
}
161197

162198
type concrete[T any] struct {
@@ -210,3 +246,5 @@ func (c *concrete[T]) UnmarshalJSON(b []byte) error {
210246
c.val = v
211247
return nil
212248
}
249+
250+
func (c *concrete[T]) EncodeRLP(w io.Writer) error { return rlp.Encode(w, c.val) }

libevm/pseudo/type_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// You should have received a copy of the GNU Lesser General Public License
1414
// along with the go-ethereum library. If not, see
1515
// <http://www.gnu.org/licenses/>.
16+
1617
package pseudo
1718

1819
import (
@@ -116,3 +117,58 @@ func TestPointer(t *testing.T) {
116117
assert.Equal(t, 314159, val.Get().payload, "after setting via pointer")
117118
})
118119
}
120+
121+
func TestIsZero(t *testing.T) {
122+
tests := []struct {
123+
typ *Type
124+
want bool
125+
}{
126+
{From(0).Type, true},
127+
{From(1).Type, false},
128+
{From("").Type, true},
129+
{From("x").Type, false},
130+
{From((*testing.T)(nil)).Type, true},
131+
{From(t).Type, false},
132+
{From(false).Type, true},
133+
{From(true).Type, false},
134+
}
135+
136+
for _, tt := range tests {
137+
assert.Equalf(t, tt.want, tt.typ.IsZero(), "%T(%[1]v) IsZero()", tt.typ.Interface())
138+
}
139+
}
140+
141+
type isEqualStub struct {
142+
isEqual bool
143+
}
144+
145+
var _ EqualityChecker[isEqualStub] = (*isEqualStub)(nil)
146+
147+
func (s isEqualStub) Equal(isEqualStub) bool {
148+
return s.isEqual
149+
}
150+
151+
func TestEqual(t *testing.T) {
152+
isEqual := isEqualStub{true}
153+
notEqual := isEqualStub{false}
154+
155+
tests := []struct {
156+
a, b *Type
157+
want bool
158+
}{
159+
{From(42).Type, From(42).Type, true},
160+
{From(99).Type, From("").Type, false},
161+
{From(false).Type, From("").Type, false}, // sorry JavaScript, you're wrong
162+
{From(isEqual).Type, From(isEqual).Type, true},
163+
{From(notEqual).Type, From(notEqual).Type, false},
164+
}
165+
166+
for _, tt := range tests {
167+
t.Run("", func(t *testing.T) {
168+
t.Logf("a = %+v", tt.a)
169+
t.Logf("b = %+v", tt.b)
170+
assert.Equal(t, tt.want, tt.a.Equal(tt.b), "a.Equals(b)")
171+
assert.Equal(t, tt.want, tt.b.Equal(tt.a), "b.Equals(a)")
172+
})
173+
}
174+
}

0 commit comments

Comments
 (0)