mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 05:41:35 +00:00
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:
parent
336a289f42
commit
f0ae9c50eb
9 changed files with 576 additions and 1 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
24
core/types/gen_slim_account_rlp.libevm.go
Normal file
24
core/types/gen_slim_account_rlp.libevm.go
Normal 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()
|
||||
}
|
||||
175
core/types/rlp_payload.libevm.go
Normal file
175
core/types/rlp_payload.libevm.go
Normal 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))
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
225
core/types/state_account.libevm_test.go
Normal file
225
core/types/state_account.libevm_test.go
Normal 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
56
libevm/pseudo/fmt.go
Normal 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
82
libevm/pseudo/fmt_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -193,6 +193,7 @@ type value interface {
|
|||
json.Unmarshaler
|
||||
rlp.Encoder
|
||||
rlp.Decoder
|
||||
fmt.Formatter
|
||||
}
|
||||
|
||||
type concrete[T any] struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue