feat: types.StateAccount pseudo-generic payload (#44)

Some of the changes in the full commit history were merged into `libevm` as part of #43 in `336a289` and then merged back into this branch as `5b15698`. 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()`

* fix: remove unnecessary `StateAccountExtra.clone()`

* refactor: readability

* feat: `pseudo.Type.Format()` implements `fmt.Formatter`
This commit is contained in:
Arran Schlosberg 2024-10-02 16:00:13 +01:00 committed by GitHub
parent 336a289f42
commit f0ae9c50eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 576 additions and 1 deletions

View file

@ -579,6 +579,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
Balance: acc.Balance,
CodeHash: acc.CodeHash,
Root: common.BytesToHash(acc.Root),
Extra: acc.Extra, // no need to deep-copy as `acc` is short-lived
}
if len(data.CodeHash) == 0 {
data.CodeHash = types.EmptyCodeHash.Bytes()

View file

@ -16,6 +16,9 @@ func (obj *StateAccount) EncodeRLP(_w io.Writer) error {
}
w.WriteBytes(obj.Root[:])
w.WriteBytes(obj.CodeHash)
if err := obj.Extra.EncodeRLP(w); err != nil {
return err
}
w.ListEnd(_tmp0)
return w.Flush()
}

View file

@ -0,0 +1,24 @@
// Code generated by rlpgen. DO NOT EDIT.
package types
import "github.com/ethereum/go-ethereum/rlp"
import "io"
func (obj *SlimAccount) EncodeRLP(_w io.Writer) error {
w := rlp.NewEncoderBuffer(_w)
_tmp0 := w.List()
w.WriteUint64(obj.Nonce)
if obj.Balance == nil {
w.Write(rlp.EmptyString)
} else {
w.WriteUint256(obj.Balance)
}
w.WriteBytes(obj.Root)
w.WriteBytes(obj.CodeHash)
if err := obj.Extra.EncodeRLP(w); err != nil {
return err
}
w.ListEnd(_tmp0)
return w.Flush()
}

View file

@ -0,0 +1,175 @@
// 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 types
import (
"fmt"
"io"
"github.com/ethereum/go-ethereum/libevm/pseudo"
"github.com/ethereum/go-ethereum/rlp"
)
// RegisterExtras registers the type `SA` to be carried as an extra payload in
// [StateAccount] structs. It is expected to be called in an `init()` function
// and MUST NOT be called more than once.
//
// The payload will be treated as an extra struct field for the purposes of RLP
// encoding and decoding. RLP handling is plumbed through to the `SA` via the
// [StateAccountExtra] that holds it such that it acts as if there were a field
// of type `SA` in all StateAccount structs.
//
// The payload can be acced via the [ExtraPayloads.FromStateAccount] method of
// the accessor returned by RegisterExtras.
func RegisterExtras[SA any]() ExtraPayloads[SA] {
if registeredExtras != nil {
panic("re-registration of Extras")
}
var extra ExtraPayloads[SA]
registeredExtras = &extraConstructors{
stateAccountType: func() string {
var x SA
return fmt.Sprintf("%T", x)
}(),
newStateAccount: pseudo.NewConstructor[SA]().Zero,
cloneStateAccount: extra.cloneStateAccount,
}
return extra
}
var registeredExtras *extraConstructors
type extraConstructors struct {
stateAccountType string
newStateAccount func() *pseudo.Type
cloneStateAccount func(*StateAccountExtra) *StateAccountExtra
}
func (e *StateAccountExtra) clone() *StateAccountExtra {
switch r := registeredExtras; {
case r == nil, e == nil:
return nil
default:
return r.cloneStateAccount(e)
}
}
// ExtraPayloads provides strongly typed access to the extra payload carried by
// [StateAccount] structs. The only valid way to construct an instance is by a
// call to [RegisterExtras].
type ExtraPayloads[SA any] struct {
_ struct{} // make godoc show unexported fields so nobody tries to make their own instance ;)
}
func (ExtraPayloads[SA]) cloneStateAccount(s *StateAccountExtra) *StateAccountExtra {
v := pseudo.MustNewValue[SA](s.t)
return &StateAccountExtra{
t: pseudo.From(v.Get()).Type,
}
}
// FromStateAccount returns the StateAccount's payload.
func (ExtraPayloads[SA]) FromStateAccount(a *StateAccount) SA {
return pseudo.MustNewValue[SA](a.extra().payload()).Get()
}
// PointerFromStateAccount returns a pointer to the StateAccounts's extra
// payload. This is guaranteed to be non-nil.
//
// Note that copying a StateAccount by dereferencing a pointer will result in a
// shallow copy and that the *SA returned here will therefore be shared by all
// copies. If this is not the desired behaviour, use
// [StateAccount.Copy] or [ExtraPayloads.SetOnStateAccount].
func (ExtraPayloads[SA]) PointerFromStateAccount(a *StateAccount) *SA {
return pseudo.MustPointerTo[SA](a.extra().payload()).Value.Get()
}
// SetOnStateAccount sets the StateAccount's payload.
func (ExtraPayloads[SA]) SetOnStateAccount(a *StateAccount, val SA) {
a.extra().t = pseudo.From(val).Type
}
// A StateAccountExtra carries the extra payload, if any, registered with
// [RegisterExtras]. It SHOULD NOT be used directly; instead use the
// [ExtraPayloads] accessor returned by RegisterExtras.
type StateAccountExtra struct {
t *pseudo.Type
}
func (a *StateAccount) extra() *StateAccountExtra {
if a.Extra == nil {
a.Extra = &StateAccountExtra{
t: registeredExtras.newStateAccount(),
}
}
return a.Extra
}
func (e *StateAccountExtra) payload() *pseudo.Type {
if e.t == nil {
e.t = registeredExtras.newStateAccount()
}
return e.t
}
var _ interface {
rlp.Encoder
rlp.Decoder
fmt.Formatter
} = (*StateAccountExtra)(nil)
// EncodeRLP implements the [rlp.Encoder] interface.
func (e *StateAccountExtra) EncodeRLP(w io.Writer) error {
switch r := registeredExtras; {
case r == nil:
return nil
case e == nil:
e = &StateAccountExtra{}
fallthrough
case e.t == nil:
e.t = r.newStateAccount()
}
return e.t.EncodeRLP(w)
}
// DecodeRLP implements the [rlp.Decoder] interface.
func (e *StateAccountExtra) DecodeRLP(s *rlp.Stream) error {
switch r := registeredExtras; {
case r == nil:
return nil
case e.t == nil:
e.t = r.newStateAccount()
fallthrough
default:
return s.Decode(e.t)
}
}
// Format implements the [fmt.Formatter] interface.
func (e *StateAccountExtra) Format(s fmt.State, verb rune) {
var out string
switch r := registeredExtras; {
case r == nil:
out = "<nil>"
case e == nil, e.t == nil:
out = fmt.Sprintf("<nil>[*StateAccountExtra[%s]]", r.stateAccountType)
default:
e.t.Format(s, verb)
return
}
_, _ = s.Write([]byte(out))
}

View file

@ -25,6 +25,7 @@ import (
)
//go:generate go run ../../rlp/rlpgen -type StateAccount -out gen_account_rlp.go
//go:generate go run ../../rlp/rlpgen -type SlimAccount -out gen_slim_account_rlp.libevm.go
// StateAccount is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
@ -33,6 +34,8 @@ type StateAccount struct {
Balance *uint256.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
Extra *StateAccountExtra
}
// NewEmptyStateAccount constructs an empty state account.
@ -55,6 +58,7 @@ func (acct *StateAccount) Copy() *StateAccount {
Balance: balance,
Root: acct.Root,
CodeHash: common.CopyBytes(acct.CodeHash),
Extra: acct.Extra.clone(),
}
}
@ -66,6 +70,8 @@ type SlimAccount struct {
Balance *uint256.Int
Root []byte // Nil if root equals to types.EmptyRootHash
CodeHash []byte // Nil if hash equals to types.EmptyCodeHash
Extra *StateAccountExtra
}
// SlimAccountRLP encodes the state account in 'slim RLP' format.
@ -73,6 +79,7 @@ func SlimAccountRLP(account StateAccount) []byte {
slim := SlimAccount{
Nonce: account.Nonce,
Balance: account.Balance,
Extra: account.Extra,
}
if account.Root != EmptyRootHash {
slim.Root = account.Root[:]
@ -80,7 +87,7 @@ func SlimAccountRLP(account StateAccount) []byte {
if !bytes.Equal(account.CodeHash, EmptyCodeHash[:]) {
slim.CodeHash = account.CodeHash
}
data, err := rlp.EncodeToBytes(slim)
data, err := rlp.EncodeToBytes(&slim)
if err != nil {
panic(err)
}
@ -96,6 +103,7 @@ func FullAccount(data []byte) (*StateAccount, error) {
}
var account StateAccount
account.Nonce, account.Balance = slim.Nonce, slim.Balance
account.Extra = slim.Extra
// Interpret the storage root and code hash in slim format.
if len(slim.Root) == 0 {

View file

@ -0,0 +1,225 @@
// 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 types
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/libevm/pseudo"
"github.com/ethereum/go-ethereum/rlp"
)
func (e *StateAccountExtra) Equal(f *StateAccountExtra) bool {
eNil := e == nil || e.t == nil
fNil := f == nil || f.t == nil
if eNil && fNil || eNil && f.t.IsZero() || fNil && e.t.IsZero() {
return true
}
return e.t.Equal(f.t)
}
func TestStateAccountRLP(t *testing.T) {
// RLP encodings that don't involve extra payloads were generated on raw
// geth StateAccounts *before* any libevm modifications, thus locking in
// default behaviour. Encodings that involve a boolean payload were
// generated on ava-labs/coreth StateAccounts to guarantee equivalence.
type test struct {
name string
register func()
acc *StateAccount
wantHex string
}
explicitFalseBoolean := test{
name: "explicit false-boolean extra",
register: func() {
RegisterExtras[bool]()
},
acc: &StateAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x666666),
Root: common.Hash{},
CodeHash: []byte{0xbb, 0xbb, 0xbb},
Extra: &StateAccountExtra{
t: pseudo.From(false).Type,
},
},
wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb80`,
}
// The vanilla geth code won't set payloads so we need to ensure that the
// zero-value encoding is used instead of the null-value default as when
// no type is registered.
implicitFalseBoolean := explicitFalseBoolean
implicitFalseBoolean.name = "implicit false-boolean extra as zero-value of registered type"
// Clearing the Extra makes the `false` value implicit and due only to the
// fact that we register `bool`. Most importantly, note that `wantHex`
// remains identical.
implicitFalseBoolean.acc.Extra = nil
tests := []test{
explicitFalseBoolean,
implicitFalseBoolean,
{
name: "true-boolean extra",
register: func() {
RegisterExtras[bool]()
},
acc: &StateAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x666666),
Root: common.Hash{},
CodeHash: []byte{0xbb, 0xbb, 0xbb},
Extra: &StateAccountExtra{
t: pseudo.From(true).Type,
},
},
wantHex: `0xee8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb01`,
},
{
name: "vanilla geth account",
acc: &StateAccount{
Nonce: 0xcccccc,
Balance: uint256.NewInt(0x555555),
Root: common.MaxHash,
CodeHash: []byte{0x77, 0x77, 0x77},
},
wantHex: `0xed83cccccc83555555a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83777777`,
},
{
name: "vanilla geth account",
acc: &StateAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x666666),
Root: common.Hash{},
CodeHash: []byte{0xbb, 0xbb, 0xbb},
},
wantHex: `0xed8344444483666666a0000000000000000000000000000000000000000000000000000000000000000083bbbbbb`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.register != nil {
registeredExtras = nil
tt.register()
t.Cleanup(func() {
registeredExtras = nil
})
}
assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex)
t.Run("RLP round trip via SlimAccount", func(t *testing.T) {
got, err := FullAccount(SlimAccountRLP(*tt.acc))
require.NoError(t, err)
if diff := cmp.Diff(tt.acc, got); diff != "" {
t.Errorf("FullAccount(SlimAccountRLP(x)) != x; diff (-want +got):\n%s", diff)
}
})
})
}
}
func assertRLPEncodingAndReturn(t *testing.T, val any, wantHex string) []byte {
t.Helper()
got, err := rlp.EncodeToBytes(val)
require.NoError(t, err, "rlp.EncodeToBytes()")
t.Logf("got RLP: %#x", got)
wantHex = strings.TrimPrefix(wantHex, "0x")
require.Equalf(t, common.Hex2Bytes(wantHex), got, "RLP encoding of %T", val)
return got
}
func TestSlimAccountRLP(t *testing.T) {
// All RLP encodings were generated on geth SlimAccounts *before* libevm
// modifications, to lock in default behaviour.
tests := []struct {
name string
acc *SlimAccount
wantHex string
}{
{
acc: &SlimAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x777777),
},
wantHex: `0xca83444444837777778080`,
},
{
acc: &SlimAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x777777),
Root: common.MaxHash[:],
},
wantHex: `0xea8344444483777777a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80`,
},
{
acc: &SlimAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x777777),
CodeHash: common.MaxHash[:],
},
wantHex: `0xea834444448377777780a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`,
},
{
acc: &SlimAccount{
Nonce: 0x444444,
Balance: uint256.NewInt(0x777777),
Root: common.MaxHash[:],
CodeHash: repeatAsHash(0xee).Bytes(),
},
wantHex: `0xf84a8344444483777777a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := assertRLPEncodingAndReturn(t, tt.acc, tt.wantHex)
got := new(SlimAccount)
require.NoError(t, rlp.DecodeBytes(buf, got), "rlp.DecodeBytes()")
opts := []cmp.Option{
// The require package differentiates between empty and nil
// slices and doesn't have a configuration mechanism.
cmpopts.EquateEmpty(),
}
if diff := cmp.Diff(tt.acc, got, opts...); diff != "" {
t.Errorf("rlp.DecodeBytes(rlp.EncodeToBytes(%T), ...) round trip; diff (-want +got):\n%s", tt.acc, diff)
}
})
}
}
func repeatAsHash(x byte) (h common.Hash) {
for i := range h {
h[i] = x
}
return h
}

56
libevm/pseudo/fmt.go Normal file
View file

@ -0,0 +1,56 @@
// 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 (
"fmt"
)
var _ = []fmt.Formatter{
(*Type)(nil),
(*Value[struct{}])(nil),
(*concrete[struct{}])(nil),
}
// Format implements the [fmt.Formatter] interface.
func (t *Type) Format(s fmt.State, verb rune) {
switch {
case t == nil, t.val == nil:
writeToFmtState(s, "<nil>[pseudo.Type[unknown]]")
default:
t.val.Format(s, verb)
}
}
// Format implements the [fmt.Formatter] interface.
func (v *Value[T]) Format(s fmt.State, verb rune) { v.t.Format(s, verb) }
func (c *concrete[T]) Format(s fmt.State, verb rune) {
switch {
case c == nil:
writeToFmtState(s, "<nil>[pseudo.Type[%T]]", concrete[T]{}.val)
default:
// Respects the original formatting directive. fmt all the way down!
format := fmt.Sprintf("pseudo.Type[%%T]{%s}", fmt.FormatString(s, verb))
writeToFmtState(s, format, c.val, c.val)
}
}
func writeToFmtState(s fmt.State, format string, a ...any) {
// There is no way to bubble errors out from a `fmt.Formatter`.
_, _ = s.Write([]byte(fmt.Sprintf(format, a...)))
}

82
libevm/pseudo/fmt_test.go Normal file
View file

@ -0,0 +1,82 @@
// 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 (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormat(t *testing.T) {
tests := []struct {
name string
from any
format string
wantContains []string
}{
{
name: "width",
from: 42,
format: "%04d",
wantContains: []string{"int", "0042"},
},
{
name: "precision",
from: float64(2),
format: "%.5f",
wantContains: []string{"float64", "2.00000"},
},
{
name: "flag",
from: 42,
format: "%+d",
wantContains: []string{"int", "+42"},
},
{
name: "verb",
from: 42,
format: "%x",
wantContains: []string{"int", "2a"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := fmt.Sprintf(tt.format, fromAny(t, tt.from))
for _, want := range tt.wantContains {
assert.Containsf(t, got, want, "fmt.Sprintf(%q, From(%T[%[2]v]))", tt.format, tt.from)
}
})
}
}
func fromAny(t *testing.T, x any) *Type {
t.Helper()
// Without this, the function will be From[any]().
switch x := x.(type) {
case int:
return From(x).Type
case float64:
return From(x).Type
default:
t.Fatalf("Bad test setup: add type case for %T", x)
return nil
}
}

View file

@ -193,6 +193,7 @@ type value interface {
json.Unmarshaler
rlp.Encoder
rlp.Decoder
fmt.Formatter
}
type concrete[T any] struct {