Skip to content

feat: params.ChainConfig and params.Rules extra payloads #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions libevm/pseudo/constructor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pseudo

// A Constructor returns newly constructed [Type] instances for a pre-registered
// concrete type.
type Constructor interface {
Zero() *Type
NewPointer() *Type
NilPointer() *Type
}

// NewConstructor returns a [Constructor] that builds `T` [Type] instances.
func NewConstructor[T any]() Constructor {
return ctor[T]{}
}

type ctor[T any] struct{}

func (ctor[T]) Zero() *Type { return Zero[T]().Type }
func (ctor[T]) NilPointer() *Type { return Zero[*T]().Type }

func (ctor[T]) NewPointer() *Type {
var x T
return From(&x).Type
}
45 changes: 45 additions & 0 deletions libevm/pseudo/constructor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package pseudo

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConstructor(t *testing.T) {
testConstructor[uint](t)
testConstructor[string](t)
testConstructor[struct{ x string }](t)
}

func testConstructor[T any](t *testing.T) {
var zero T
t.Run(fmt.Sprintf("%T", zero), func(t *testing.T) {
ctor := NewConstructor[T]()

t.Run("NilPointer()", func(t *testing.T) {
got := get[*T](t, ctor.NilPointer())
assert.Nil(t, got)
})

t.Run("NewPointer()", func(t *testing.T) {
got := get[*T](t, ctor.NewPointer())
require.NotNil(t, got)
assert.Equal(t, zero, *got)
})

t.Run("Zero()", func(t *testing.T) {
got := get[T](t, ctor.Zero())
assert.Equal(t, zero, got)
})
})
}

func get[T any](t *testing.T, typ *Type) (x T) {
t.Helper()
val, err := NewValue[T](typ)
require.NoError(t, err, "NewValue[%T]()", x)
return val.Get()
}
175 changes: 175 additions & 0 deletions libevm/pseudo/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Package pseudo provides a bridge between generic and non-generic code via
// pseudo-types and pseudo-values. With careful usage, there is minimal
// reduction in type safety.
//
// Adding generic type parameters to anything (e.g. struct, function, etc)
// "pollutes" all code that uses the generic type. Refactoring all uses isn't
// always feasible, and a [Type] acts as an intermediate fix. Although their
// constructors are generic, they are not, and they are instead coupled with a
// generic [Value] that SHOULD be used for access.
//
// Packages typically SHOULD NOT expose a [Type] and SHOULD instead provide
// users with a type-safe [Value].
package pseudo

import (
"encoding/json"
"fmt"
)

// A Type wraps a strongly-typed value without exposing information about its
// type. It can be used in lieu of a generic field / parameter.
type Type struct {
val value
}

// A Value provides strongly-typed access to the payload carried by a [Type].
type Value[T any] struct {
t *Type
}

// A Pseudo type couples a [Type] and a [Value]. If returned by a constructor
// from this package, both wrap the same payload.
type Pseudo[T any] struct {
Type *Type
Value *Value[T]
}

// TypeAndValue is a convenience function for splitting the contents of `p`,
// typically at construction.
func (p *Pseudo[T]) TypeAndValue() (*Type, *Value[T]) {
return p.Type, p.Value
}

// From returns a Pseudo[T] constructed from `v`.
func From[T any](v T) *Pseudo[T] {
t := &Type{
val: &concrete[T]{
val: v,
},
}
return &Pseudo[T]{t, MustNewValue[T](t)}
}

// Zero is equivalent to [From] called with the [zero value] of type `T`. Note
// that pointers, slices, maps, etc. will therefore be nil.
//
// [zero value]: https://go.dev/tour/basics/12
func Zero[T any]() *Pseudo[T] {
var x T
return From[T](x)
}

// Interface returns the wrapped value as an `any`, equivalent to
// [reflect.Value.Interface]. Prefer [Value.Get].
func (t *Type) Interface() any { return t.val.get() }

// NewValue constructs a [Value] from a [Type], first confirming that `t` wraps
// a payload of type `T`.
func NewValue[T any](t *Type) (*Value[T], error) {
var x T
if !t.val.canSetTo(x) {
return nil, fmt.Errorf("cannot create *Value[%T] with *Type carrying %T", x, t.val.get())
}
return &Value[T]{t}, nil
}

// MustNewValue is equivalent to [NewValue] except that it panics instead of
// returning an error.
func MustNewValue[T any](t *Type) *Value[T] {
v, err := NewValue[T](t)
if err != nil {
panic(err)
}
return v
}

// Get returns the value.
func (a *Value[T]) Get() T { return a.t.val.get().(T) }

// Set sets the value.
func (a *Value[T]) Set(v T) { a.t.val.mustSet(v) }

// MarshalJSON implements the [json.Marshaler] interface.
func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() }

// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) }

// MarshalJSON implements the [json.Marshaler] interface.
func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() }

// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) }

var _ = []interface {
json.Marshaler
json.Unmarshaler
}{
(*Type)(nil),
(*Value[struct{}])(nil),
(*concrete[struct{}])(nil),
}

// A value is a non-generic wrapper around a [concrete] struct.
type value interface {
get() any
canSetTo(any) bool
set(any) error
mustSet(any)

json.Marshaler
json.Unmarshaler
}

type concrete[T any] struct {
val T
}

func (c *concrete[T]) get() any { return c.val }

func (c *concrete[T]) canSetTo(v any) bool {
_, ok := v.(T)
return ok
}

// An invalidTypeError is returned by [conrete.set] if the value is incompatible
// with its type. This should never leave this package and exists only to
// provide precise testing of unhappy paths.
type invalidTypeError[T any] struct {
SetTo any
}

func (e *invalidTypeError[T]) Error() string {
var t T
return fmt.Sprintf("cannot set %T to %T", t, e.SetTo)
}

func (c *concrete[T]) set(v any) error {
vv, ok := v.(T)
if !ok {
// Other invariants in this implementation (aim to) guarantee that this
// will never happen.
return &invalidTypeError[T]{SetTo: v}
}
c.val = vv
return nil
}

func (c *concrete[T]) mustSet(v any) {
if err := c.set(v); err != nil {
panic(err)
}
_ = 0 // for happy-path coverage inspection
}

func (c *concrete[T]) MarshalJSON() ([]byte, error) { return json.Marshal(c.val) }

func (c *concrete[T]) UnmarshalJSON(b []byte) error {
var v T
if err := json.Unmarshal(b, &v); err != nil {
return err
}
c.val = v
return nil
}
79 changes: 79 additions & 0 deletions libevm/pseudo/type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package pseudo

import (
"encoding/json"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestType(t *testing.T) {
testType(t, "Zero[int]", Zero[int], 0, 42, "I'm not an int")
testType(t, "Zero[string]", Zero[string], "", "hello, world", 99)

testType(
t, "From[uint](314159)",
func() *Pseudo[uint] {
return From[uint](314159)
},
314159, 0, struct{}{},
)

testType(t, "nil pointer", Zero[*float64], (*float64)(nil), new(float64), 0)
}

func testType[T any](t *testing.T, name string, ctor func() *Pseudo[T], init T, setTo T, invalid any) {
t.Run(name, func(t *testing.T) {
typ, val := ctor().TypeAndValue()
assert.Equal(t, init, val.Get())
val.Set(setTo)
assert.Equal(t, setTo, val.Get())

t.Run("set to invalid type", func(t *testing.T) {
wantErr := &invalidTypeError[T]{SetTo: invalid}

assertError := func(t *testing.T, err any) {
t.Helper()
switch err := err.(type) {
case *invalidTypeError[T]:
assert.Equal(t, wantErr, err)
default:
t.Errorf("got error %v; want %v", err, wantErr)
}
}

t.Run(fmt.Sprintf("Set(%T{%v})", invalid, invalid), func(t *testing.T) {
assertError(t, typ.val.set(invalid))
})

t.Run(fmt.Sprintf("MustSet(%T{%v})", invalid, invalid), func(t *testing.T) {
defer func() {
assertError(t, recover())
}()
typ.val.mustSet(invalid)
})
})

t.Run("JSON round trip", func(t *testing.T) {
buf, err := json.Marshal(typ)
require.NoError(t, err)

got, gotVal := Zero[T]().TypeAndValue()
require.NoError(t, json.Unmarshal(buf, &got))
assert.Equal(t, val.Get(), gotVal.Get())
})
})
}

func ExamplePseudo_TypeAndValue() {
typ, val := From("hello").TypeAndValue()

// But, if only one is needed:
typ = From("world").Type
val = From("this isn't coupled to the Type").Value

_ = typ
_ = val
}
9 changes: 8 additions & 1 deletion params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/libevm/pseudo"
"github.com/ethereum/go-ethereum/params/forks"
)

Expand Down Expand Up @@ -365,6 +366,8 @@ type ChainConfig struct {
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
Clique *CliqueConfig `json:"clique,omitempty"`

extra *pseudo.Type // See RegisterExtras()
}

// EthashConfig is the consensus engine configs for proof-of-work based sealing.
Expand Down Expand Up @@ -902,6 +905,8 @@ type Rules struct {
IsBerlin, IsLondon bool
IsMerge, IsShanghai, IsCancun, IsPrague bool
IsVerkle bool

extra *pseudo.Type // See RegisterExtras()
}

// Rules ensures c's ChainID is not nil.
Expand All @@ -912,7 +917,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
}
// disallow setting Merge out of order
isMerge = isMerge && c.IsLondon(num)
return Rules{
r := Rules{
ChainID: new(big.Int).Set(chainID),
IsHomestead: c.IsHomestead(num),
IsEIP150: c.IsEIP150(num),
Expand All @@ -930,4 +935,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules
IsPrague: isMerge && c.IsPrague(num, timestamp),
IsVerkle: isMerge && c.IsVerkle(num, timestamp),
}
c.addRulesExtra(&r, num, isMerge, timestamp)
return r
}
Loading