go-ethereum/rlp/fields.libevm_test.go
Arran Schlosberg d210cc4fce
refactor(core/types): simplify Body RLP override (#120)
## Why this should be merged

Simplification of `types.Body` RLP overriding, resulting in reduced code
at both the implementation and consumer ends.

## How this works

Introduction of `rlp.Fields` type, to mirror regular RLP encoding of a
struct. The RLP override hook now only needs to return the fields of
interest, which MAY come from either the `Body` or the registered extra.

This pattern allows for arbitrary modification of upstream fields via
(1) reordering; (2) addition; (3) deletion; and (4) inverting required
vs optional status. While less important for `Body`, this allows for
complete support of `ava-labs/coreth` `Header` modifications, which make
use of 1-3.

## How this was tested

Existing backwards-compatibility tests + new unit tests for introduced
functionality.

---------

Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com>
Co-authored-by: Quentin McGaw <quentin.mcgaw@avalabs.org>
2025-02-07 15:42:25 +00:00

218 lines
5.3 KiB
Go

// Copyright 2025 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 rlp
import (
"bytes"
"io"
"testing"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ava-labs/libevm/common"
)
func TestFields(t *testing.T) {
type foo struct {
A uint64
B uint64
C *uint64
D *uint64 `rlp:"optional"`
E []uint64 `rlp:"optional"`
F *[]uint64 `rlp:"optional"`
}
const (
a uint64 = iota
b
cVal
dVal
)
c := common.PointerTo(cVal)
d := common.PointerTo(dVal)
e := []uint64{40, 41}
f := &[]uint64{50, 51}
tests := []foo{
{a, b, c, d, e, f}, // 000 (which of d/e/f are nil)
{a, b, c, d, e, nil}, // 001
{a, b, c, d, nil, f}, // 010
{a, b, c, d, nil, nil}, // 011
{a, b, c, nil, e, f}, // 100
{a, b, c, nil, e, nil}, // 101
{a, b, c, nil, nil, f}, // 110
{a, b, c, nil, nil, nil}, // 111
// Empty and nil slices are treated differently when optional
{a, b, c, nil, []uint64{}, nil},
{a, b, c, nil, nil, &[]uint64{}},
}
for _, obj := range tests {
t.Run("", func(t *testing.T) {
t.Logf("\n%s", pretty.Sprint(obj))
wantRLP, err := EncodeToBytes(obj)
require.NoErrorf(t, err, "EncodeToBytes([actual struct])")
t.Run("Fields.EncodeRLP", func(t *testing.T) {
got, err := EncodeToBytes(&Fields{
Required: []any{obj.A, obj.B, obj.C},
Optional: []any{obj.D, obj.E, obj.F},
})
require.NoError(t, err)
assert.Equal(t, wantRLP, got, "vs EncodeToBytes([actual struct])")
})
t.Run("Fields.DecodeRLP", func(t *testing.T) {
var got foo
err := DecodeBytes(wantRLP, &Fields{
Required: []any{&got.A, &got.B, &got.C},
Optional: []any{&got.D, &got.E, &got.F},
})
require.NoError(t, err, "DecodeBytes(..., %T)", &Fields{})
var want foo
err = DecodeBytes(wantRLP, &want)
require.NoError(t, err, "DecodeBytes(..., [actual struct])")
assert.Equal(t, want, got, "vs DecodeBytes(..., [original struct])")
})
})
}
}
//nolint:testableexamples // Demonstrating code equivalence, not outputs.
func ExampleFields() {
type inner struct {
X uint64
}
type outer struct {
A uint64
B *inner `rlp:"nil"`
C *inner `rlp:"optional"`
}
val := outer{
A: 42,
B: &inner{X: 42},
C: &inner{X: 99},
}
// Errors are dropped for brevity for the sake of the example only.
_ = Encode(io.Discard, val)
// is equivalent to
_ = Encode(
io.Discard,
&Fields{
Required: []any{val.A, val.B},
Optional: []any{val.C},
},
)
var (
r *bytes.Reader // arbitrary RLP buffer
decoded outer
)
_ = Decode(r, &decoded)
// is equivalent to
_ = Decode(r, &Fields{
Required: []any{
&decoded.A,
Nillable(&decoded.B),
},
Optional: []any{&decoded.C},
})
// Note the parallels between the arguments passed to
// Fields.{En,De}codeRLP() and that, when decoding an optional or
// `rlp:"nil`-tagged field, a pointer to the _field_ is required even though
// in this example it will be a `**inner`.
}
func TestNillable(t *testing.T) {
type inner struct {
X uint64
}
type outer struct {
A *uint64 `rlp:"nil"`
B *inner `rlp:"nil"`
C *[]uint64 `rlp:"nil"`
}
// Unlike the `rlp:"optional"` tag, there is no interplay between nil-tagged
// fields so we don't need the Cartesian product of all possible
// combinations.
var tests []outer
for _, a := range []*uint64{
nil,
common.PointerTo[uint64](0),
} {
tests = append(tests, outer{a, nil, nil})
}
for _, b := range []*inner{
nil,
{0},
} {
tests = append(tests, outer{nil, b, nil})
}
for _, c := range []*[]uint64{
nil,
{},
{0},
} {
tests = append(tests, outer{nil, nil, c})
}
// When a Nillable encounters an empty list it MUST set the field to nil,
// not just ignore it.
corruptInitialValue := func() outer {
return outer{common.PointerTo[uint64](42), &inner{42}, &[]uint64{42}}
}
for _, obj := range tests {
t.Run("", func(t *testing.T) {
rlp, err := EncodeToBytes(obj)
require.NoErrorf(t, err, "EncodeToBytes(%+v)", obj)
t.Logf("%s => %#x", pretty.Sprint(obj), rlp)
// Although this is an immediate inversion of the line above, it
// provides us with the canonical RLP decoding, which our input
// struct may not honour.
want := corruptInitialValue()
err = DecodeBytes(rlp, &want)
require.NoErrorf(t, err, "DecodeBytes(%#x, %T)", rlp, &want)
got := corruptInitialValue()
err = DecodeBytes(rlp, &Fields{
Required: []any{
Nillable(&got.A),
Nillable(&got.B),
Nillable(&got.C),
},
})
require.NoErrorf(t, err, "DecodeBytes(..., %T)", &Fields{})
assert.Equal(t, want, got, "DecodeBytes(..., [actual struct]) vs DecodeBytes(..., [fields wrapped in Nillable()])")
})
}
}