mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 13:44:31 +00:00
## 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>
218 lines
5.3 KiB
Go
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()])")
|
|
})
|
|
}
|
|
}
|