From 5429fd87c8764fc6c44a9ebdfdcb2ee84a881259 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:20:32 +0100 Subject: [PATCH] chore: squash `arr4n/libevm` into `libevm` (#7) * feat: pseudo-generic extra payloads in `params.ChainConfig` and `params.Rules` * feat: `params.ExtraPayloadGetter` for end-user type safety * refactor: payloads only available through `params.ExtraPayloadGetter` * chore: make `libevm/examples/extraparams` a `params` testable example * doc: `libevm/pseudo` package comments and improved readability * doc: `params.*Extra*` comments and improved readability * doc: `params.ExtraPayloadGetter` comments and improved readability * doc: `params/config.libevm_test.go` comments and improved readability * refactor: simplify `params.ChainConfig.UnmarshalJSON()` * refactor: abstract new/nil-pointer creation into `pseudo.Constructor`s * feat: precompile override via `params.Extras` hooks * doc: flesh out `PrecompileOverride()` in example * doc: complete commentary and improve readability * refactor: `ChainConfig.Hooks()` + `Rules` equivalent * chore: rename precompiles test file in keeping with geth equivalent * feat: stateful precompiles + allowlist hooks The allowlist hooks are included in this commit because they allow for the same functionality as stateful precompiles in `ava-labs/coreth` and `ava-labs/subnet-evm`. * fix: `StateTransition.canExecuteTransaction()` used `msg.From` instead of `To` * test: `params.RulesHooks.CanCreateContract` integration * test: `params.RulesHooks.CanExecuteTransaction` integration * test: `vm.NewStatefulPrecompile()` integration * refactor: simplify test of `CanCreateContract` * refactor: abstract generation of random `Address`/`Hash` values * doc: full documentation + readability refactoring/renaming * fix: remove circular dependency in tests --- core/state_transition.go | 3 + core/state_transition.libevm.go | 9 ++ core/state_transition.libevm_test.go | 40 +++++ core/vm/contracts.go | 4 +- core/vm/contracts.libevm.go | 83 ++++++++++ core/vm/contracts.libevm_test.go | 178 ++++++++++++++++++++ core/vm/evm.go | 20 ++- core/vm/libevm_test.go | 7 + libevm/ethtest/evm.go | 37 +++++ libevm/ethtest/rand.go | 41 +++++ libevm/hookstest/stub.go | 60 +++++++ libevm/interfaces_test.go | 20 +++ libevm/libevm.go | 52 ++++++ libevm/pseudo/constructor.go | 24 +++ libevm/pseudo/constructor_test.go | 45 ++++++ libevm/pseudo/type.go | 175 ++++++++++++++++++++ libevm/pseudo/type_test.go | 79 +++++++++ params/config.go | 9 +- params/config.libevm.go | 232 +++++++++++++++++++++++++++ params/config.libevm_test.go | 164 +++++++++++++++++++ params/example.libevm_test.go | 158 ++++++++++++++++++ params/hooks.libevm.go | 83 ++++++++++ 22 files changed, 1516 insertions(+), 7 deletions(-) create mode 100644 core/state_transition.libevm.go create mode 100644 core/state_transition.libevm_test.go create mode 100644 core/vm/contracts.libevm.go create mode 100644 core/vm/contracts.libevm_test.go create mode 100644 core/vm/libevm_test.go create mode 100644 libevm/ethtest/evm.go create mode 100644 libevm/ethtest/rand.go create mode 100644 libevm/hookstest/stub.go create mode 100644 libevm/interfaces_test.go create mode 100644 libevm/libevm.go create mode 100644 libevm/pseudo/constructor.go create mode 100644 libevm/pseudo/constructor_test.go create mode 100644 libevm/pseudo/type.go create mode 100644 libevm/pseudo/type_test.go create mode 100644 params/config.libevm.go create mode 100644 params/config.libevm_test.go create mode 100644 params/example.libevm_test.go create mode 100644 params/hooks.libevm.go diff --git a/core/state_transition.go b/core/state_transition.go index 9c4f76d1c585..0be28b4a4e12 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -365,6 +365,9 @@ func (st *StateTransition) preCheck() error { // However if any consensus issue encountered, return the error directly with // nil evm execution result. func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { + if err := st.canExecuteTransaction(); err != nil { + return nil, err + } // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses // diff --git a/core/state_transition.libevm.go b/core/state_transition.libevm.go new file mode 100644 index 000000000000..4d2ee16f0207 --- /dev/null +++ b/core/state_transition.libevm.go @@ -0,0 +1,9 @@ +package core + +// canExecuteTransaction is a convenience wrapper for calling the +// [params.RulesHooks.CanExecuteTransaction] hook. +func (st *StateTransition) canExecuteTransaction() error { + bCtx := st.evm.Context + rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time) + return rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state) +} diff --git a/core/state_transition.libevm_test.go b/core/state_transition.libevm_test.go new file mode 100644 index 000000000000..d81e9ce4f763 --- /dev/null +++ b/core/state_transition.libevm_test.go @@ -0,0 +1,40 @@ +package core_test + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/libevm" + "github.com/ethereum/go-ethereum/libevm/ethtest" + "github.com/ethereum/go-ethereum/libevm/hookstest" + "github.com/stretchr/testify/require" +) + +func TestCanExecuteTransaction(t *testing.T) { + rng := ethtest.NewPseudoRand(42) + account := rng.Address() + slot := rng.Hash() + + makeErr := func(from common.Address, to *common.Address, val common.Hash) error { + return fmt.Errorf("From: %v To: %v State: %v", from, to, val) + } + hooks := &hookstest.Stub{ + CanExecuteTransactionFn: func(from common.Address, to *common.Address, s libevm.StateReader) error { + return makeErr(from, to, s.GetState(account, slot)) + }, + } + hooks.RegisterForRules(t) + + value := rng.Hash() + + state, evm := ethtest.NewZeroEVM(t) + state.SetState(account, slot, value) + msg := &core.Message{ + From: rng.Address(), + To: rng.AddressPtr(), + } + _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6)) + require.EqualError(t, err, makeErr(msg.From, msg.To, value).Error()) +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 33a867654e71..ee1308fa7087 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -168,13 +168,13 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - the returned bytes, // - the _remaining_ gas, // - any error that occurred -func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { +func (args *evmCallArgs) RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { gasCost := p.RequiredGas(input) if suppliedGas < gasCost { return nil, 0, ErrOutOfGas } suppliedGas -= gasCost - output, err := p.Run(input) + output, err := args.run(p, input) return output, suppliedGas, err } diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go new file mode 100644 index 000000000000..23057d6f9c8c --- /dev/null +++ b/core/vm/contracts.libevm.go @@ -0,0 +1,83 @@ +package vm + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(), +// DelegateCall() and StaticCall(). Its fields are identical to those of the +// parameters, prepended with the receiver name. As {Delegate,Static}Call don't +// accept a value, they MUST set the respective field to nil. +// +// Instantiation can be achieved by merely copying the parameter names, in +// order, which is trivially achieved with AST manipulation: +// +// func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) ... { +// ... +// args := &evmCallArgs{evm, caller, addr, input, gas, value} +type evmCallArgs struct { + evm *EVM + caller ContractRef + addr common.Address + input []byte + gas uint64 + value *uint256.Int +} + +// run runs the [PrecompiledContract], differentiating between stateful and +// regular types. +func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) { + if p, ok := p.(statefulPrecompile); ok { + return p.run(args.evm.StateDB, &args.evm.chainRules, args.caller.Address(), args.addr, input) + } + return p.Run(input) +} + +// PrecompiledStatefulRun is the stateful equivalent of the Run() method of a +// [PrecompiledContract]. +type PrecompiledStatefulRun func(_ StateDB, _ *params.Rules, caller, self common.Address, input []byte) ([]byte, error) + +// NewStatefulPrecompile constructs a new PrecompiledContract that can be used +// via an [EVM] instance but MUST NOT be called directly; a direct call to Run() +// reserves the right to panic. See other requirements defined in the comments +// on [PrecompiledContract]. +func NewStatefulPrecompile(run PrecompiledStatefulRun, requiredGas func([]byte) uint64) PrecompiledContract { + return statefulPrecompile{ + gas: requiredGas, + run: run, + } +} + +type statefulPrecompile struct { + gas func([]byte) uint64 + run PrecompiledStatefulRun +} + +func (p statefulPrecompile) RequiredGas(input []byte) uint64 { + return p.gas(input) +} + +func (p statefulPrecompile) Run([]byte) ([]byte, error) { + // https://google.github.io/styleguide/go/best-practices.html#when-to-panic + // This would indicate an API misuse and would occur in tests, not in + // production. + panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T", p, p.run)) +} + +var ( + // These lock in the assumptions made when implementing [evmCallArgs]. If + // these break then the struct fields SHOULD be changed to match these + // signatures. + _ = [](func(ContractRef, common.Address, []byte, uint64, *uint256.Int) ([]byte, uint64, error)){ + (*EVM)(nil).Call, + (*EVM)(nil).CallCode, + } + _ = [](func(ContractRef, common.Address, []byte, uint64) ([]byte, uint64, error)){ + (*EVM)(nil).DelegateCall, + (*EVM)(nil).StaticCall, + } +) diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go new file mode 100644 index 000000000000..28bdcde09a52 --- /dev/null +++ b/core/vm/contracts.libevm_test.go @@ -0,0 +1,178 @@ +package vm_test + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/libevm" + "github.com/ethereum/go-ethereum/libevm/ethtest" + "github.com/ethereum/go-ethereum/libevm/hookstest" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +type precompileStub struct { + requiredGas uint64 + returnData []byte +} + +func (s *precompileStub) RequiredGas([]byte) uint64 { return s.requiredGas } +func (s *precompileStub) Run([]byte) ([]byte, error) { return s.returnData, nil } + +func TestPrecompileOverride(t *testing.T) { + type test struct { + name string + addr common.Address + requiredGas uint64 + stubData []byte + } + + const gasLimit = uint64(1e7) + + tests := []test{ + { + name: "arbitrary values", + addr: common.Address{'p', 'r', 'e', 'c', 'o', 'm', 'p', 'i', 'l', 'e'}, + requiredGas: 314159, + stubData: []byte("the return data"), + }, + } + + rng := rand.New(rand.NewSource(42)) + for _, addr := range vm.PrecompiledAddressesCancun { + tests = append(tests, test{ + name: fmt.Sprintf("existing precompile %v", addr), + addr: addr, + requiredGas: rng.Uint64n(gasLimit), + stubData: addr[:], + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + tt.addr: &precompileStub{ + requiredGas: tt.requiredGas, + returnData: tt.stubData, + }, + }, + } + hooks.RegisterForRules(t) + + t.Run(fmt.Sprintf("%T.Call([overridden precompile address = %v])", &vm.EVM{}, tt.addr), func(t *testing.T) { + _, evm := ethtest.NewZeroEVM(t) + gotData, gotGasLeft, err := evm.Call(vm.AccountRef{}, tt.addr, nil, gasLimit, uint256.NewInt(0)) + require.NoError(t, err) + assert.Equal(t, tt.stubData, gotData, "contract's return data") + assert.Equal(t, gasLimit-tt.requiredGas, gotGasLeft, "gas left") + }) + }) + } +} + +func TestNewStatefulPrecompile(t *testing.T) { + rng := ethtest.NewPseudoRand(314159) + precompile := rng.Address() + slot := rng.Hash() + + const gasLimit = 1e6 + gasCost := rng.Uint64n(gasLimit) + + makeOutput := func(caller, self common.Address, input []byte, stateVal common.Hash) []byte { + return []byte(fmt.Sprintf( + "Caller: %v Precompile: %v State: %v Input: %#x", + caller, self, stateVal, input, + )) + } + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + precompile: vm.NewStatefulPrecompile( + func(state vm.StateDB, _ *params.Rules, caller, self common.Address, input []byte) ([]byte, error) { + return makeOutput(caller, self, input, state.GetState(precompile, slot)), nil + }, + func(b []byte) uint64 { + return gasCost + }, + ), + }, + } + hooks.RegisterForRules(t) + + caller := rng.Address() + input := rng.Bytes(8) + value := rng.Hash() + + state, evm := ethtest.NewZeroEVM(t) + state.SetState(precompile, slot, value) + wantReturnData := makeOutput(caller, precompile, input, value) + wantGasLeft := gasLimit - gasCost + + gotReturnData, gotGasLeft, err := evm.Call(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0)) + require.NoError(t, err) + assert.Equal(t, wantReturnData, gotReturnData) + assert.Equal(t, wantGasLeft, gotGasLeft) +} + +func TestCanCreateContract(t *testing.T) { + rng := ethtest.NewPseudoRand(142857) + account := rng.Address() + slot := rng.Hash() + + makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error { + return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.Caller, cc.Self, stateVal) + } + hooks := &hookstest.Stub{ + CanCreateContractFn: func(cc *libevm.AddressContext, s libevm.StateReader) error { + return makeErr(cc, s.GetState(account, slot)) + }, + } + hooks.RegisterForRules(t) + + origin := rng.Address() + caller := rng.Address() + value := rng.Hash() + code := rng.Bytes(8) + salt := rng.Hash() + + create := crypto.CreateAddress(caller, 0) + create2 := crypto.CreateAddress2(caller, salt, crypto.Keccak256(code)) + + tests := []struct { + name string + create func(*vm.EVM) ([]byte, common.Address, uint64, error) + wantErr error + }{ + { + name: "Create", + create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { + return evm.Create(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0)) + }, + wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create}, value), + }, + { + name: "Create2", + create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { + return evm.Create2(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:])) + }, + wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create2}, value), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state, evm := ethtest.NewZeroEVM(t) + state.SetState(account, slot, value) + evm.TxContext.Origin = origin + + _, _, _, err := tt.create(evm) + require.EqualError(t, err, tt.wantErr.Error()) + }) + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 16cc8549080a..7b830a343a7a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/libevm" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -38,6 +39,9 @@ type ( ) func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { + if p, override := evm.chainRules.Hooks().PrecompileOverride(addr); override { + return p, p != nil + } var precompiles map[common.Address]PrecompiledContract switch { case evm.chainRules.IsCancun: @@ -224,7 +228,8 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + args := &evmCallArgs{evm, caller, addr, input, gas, value} + ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -287,7 +292,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + args := &evmCallArgs{evm, caller, addr, input, gas, value} + ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -332,7 +338,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + args := &evmCallArgs{evm, caller, addr, input, gas, nil} + ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -381,7 +388,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte } if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + args := &evmCallArgs{evm, caller, addr, input, gas, nil} + ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' @@ -420,6 +428,10 @@ func (c *codeAndHash) Hash() common.Hash { // create creates a new contract using code as deployment code. func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { + cc := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: address} + if err := evm.chainRules.Hooks().CanCreateContract(cc, evm.StateDB); err != nil { + return nil, common.Address{}, gas, err + } // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { diff --git a/core/vm/libevm_test.go b/core/vm/libevm_test.go new file mode 100644 index 000000000000..5c2fbb02a142 --- /dev/null +++ b/core/vm/libevm_test.go @@ -0,0 +1,7 @@ +package vm + +// The original RunPrecompiledContract was migrated to being a method on +// [evmCallArgs]. We need to replace it for use by regular geth tests. +func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + return (*evmCallArgs)(nil).RunPrecompiledContract(p, input, suppliedGas) +} diff --git a/libevm/ethtest/evm.go b/libevm/ethtest/evm.go new file mode 100644 index 000000000000..069a949ac640 --- /dev/null +++ b/libevm/ethtest/evm.go @@ -0,0 +1,37 @@ +// Package ethtest provides utility functions for use in testing +// Ethereum-related functionality. +package ethtest + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// NewZeroEVM returns a new EVM backed by a [rawdb.NewMemoryDatabase]; all other +// arguments to [vm.NewEVM] are the zero values of their respective types, +// except for the use of [core.CanTransfer] and [core.Transfer] instead of nil +// functions. +func NewZeroEVM(tb testing.TB) (*state.StateDB, *vm.EVM) { + tb.Helper() + + sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + require.NoError(tb, err, "state.New()") + + return sdb, vm.NewEVM( + vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + }, + vm.TxContext{}, + sdb, + ¶ms.ChainConfig{}, + vm.Config{}, + ) +} diff --git a/libevm/ethtest/rand.go b/libevm/ethtest/rand.go new file mode 100644 index 000000000000..dfabcfedca14 --- /dev/null +++ b/libevm/ethtest/rand.go @@ -0,0 +1,41 @@ +package ethtest + +import ( + "github.com/ethereum/go-ethereum/common" + "golang.org/x/exp/rand" +) + +// PseudoRand extends [rand.Rand] (*not* crypto/rand). +type PseudoRand struct { + *rand.Rand +} + +// NewPseudoRand returns a new PseudoRand with the given seed. +func NewPseudoRand(seed uint64) *PseudoRand { + return &PseudoRand{rand.New(rand.NewSource(seed))} +} + +// Address returns a pseudorandom address. +func (r *PseudoRand) Address() (a common.Address) { + r.Read(a[:]) + return a +} + +// AddressPtr returns a pointer to a pseudorandom address. +func (r *PseudoRand) AddressPtr() *common.Address { + a := r.Address() + return &a +} + +// Hash returns a pseudorandom hash. +func (r *PseudoRand) Hash() (h common.Hash) { + r.Read(h[:]) + return h +} + +// Bytes returns `n` pseudorandom bytes. +func (r *PseudoRand) Bytes(n uint) []byte { + b := make([]byte, n) + r.Read(b) + return b +} diff --git a/libevm/hookstest/stub.go b/libevm/hookstest/stub.go new file mode 100644 index 000000000000..3dcb1619c3cd --- /dev/null +++ b/libevm/hookstest/stub.go @@ -0,0 +1,60 @@ +// Package hookstest provides test doubles for testing subsets of libevm hooks. +package hookstest + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/libevm" + "github.com/ethereum/go-ethereum/params" +) + +// A Stub is a test double for [params.ChainConfigHooks] and +// [params.RulesHooks]. Each of the fields, if non-nil, back their respective +// hook methods, which otherwise fall back to the default behaviour. +type Stub struct { + PrecompileOverrides map[common.Address]libevm.PrecompiledContract + CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error + CanCreateContractFn func(*libevm.AddressContext, libevm.StateReader) error +} + +// RegisterForRules clears any registered [params.Extras] and then registers s +// as [params.RulesHooks], which are themselves cleared by the +// [testing.TB.Cleanup] routine. +func (s *Stub) RegisterForRules(tb testing.TB) { + params.TestOnlyClearRegisteredExtras() + params.RegisterExtras(params.Extras[params.NOOPHooks, Stub]{ + NewRules: func(_ *params.ChainConfig, _ *params.Rules, _ *params.NOOPHooks, blockNum *big.Int, isMerge bool, timestamp uint64) *Stub { + return s + }, + }) + tb.Cleanup(params.TestOnlyClearRegisteredExtras) +} + +func (s Stub) PrecompileOverride(a common.Address) (libevm.PrecompiledContract, bool) { + if len(s.PrecompileOverrides) == 0 { + return nil, false + } + p, ok := s.PrecompileOverrides[a] + return p, ok +} + +func (s Stub) CanExecuteTransaction(from common.Address, to *common.Address, sr libevm.StateReader) error { + if f := s.CanExecuteTransactionFn; f != nil { + return f(from, to, sr) + } + return nil +} + +func (s Stub) CanCreateContract(cc *libevm.AddressContext, sr libevm.StateReader) error { + if f := s.CanCreateContractFn; f != nil { + return f(cc, sr) + } + return nil +} + +var _ interface { + params.ChainConfigHooks + params.RulesHooks +} = Stub{} diff --git a/libevm/interfaces_test.go b/libevm/interfaces_test.go new file mode 100644 index 000000000000..05497ed40ab5 --- /dev/null +++ b/libevm/interfaces_test.go @@ -0,0 +1,20 @@ +package libevm_test + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/libevm" +) + +// IMPORTANT: if any of these break then the libevm copy MUST be updated. + +// These two interfaces MUST be identical. +var ( + // Each assignment demonstrates that the methods of the LHS interface are a + // (non-strict) subset of the RHS interface's; both being possible + // proves that they are identical. + _ vm.PrecompiledContract = (libevm.PrecompiledContract)(nil) + _ libevm.PrecompiledContract = (vm.PrecompiledContract)(nil) +) + +// StateReader MUST be a subset vm.StateDB. +var _ libevm.StateReader = (vm.StateDB)(nil) diff --git a/libevm/libevm.go b/libevm/libevm.go new file mode 100644 index 000000000000..8b7d85bd49e8 --- /dev/null +++ b/libevm/libevm.go @@ -0,0 +1,52 @@ +package libevm + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" +) + +// PrecompiledContract is an exact copy of vm.PrecompiledContract, mirrored here +// for instances where importing that package would result in a circular +// dependency. +type PrecompiledContract interface { + RequiredGas(input []byte) uint64 + Run(input []byte) ([]byte, error) +} + +// StateReader is a subset of vm.StateDB, exposing only methods that read from +// but do not modify state. See method comments in vm.StateDB, which aren't +// copied here as they risk becoming outdated. +type StateReader interface { + GetBalance(common.Address) *uint256.Int + GetNonce(common.Address) uint64 + + GetCodeHash(common.Address) common.Hash + GetCode(common.Address) []byte + GetCodeSize(common.Address) int + + GetRefund() uint64 + + GetCommittedState(common.Address, common.Hash) common.Hash + GetState(common.Address, common.Hash) common.Hash + + GetTransientState(addr common.Address, key common.Hash) common.Hash + + HasSelfDestructed(common.Address) bool + + Exist(common.Address) bool + Empty(common.Address) bool + + AddressInAccessList(addr common.Address) bool + SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) +} + +// AddressContext carries addresses available to contexts such as calls and +// contract creation. +// +// With respect to contract creation, the Self address MAY be the predicted +// address of the contract about to be deployed, which may not exist yet. +type AddressContext struct { + Origin common.Address // equivalent to vm.ORIGIN op code + Caller common.Address // equivalent to vm.CALLER op code + Self common.Address // equivalent to vm.ADDRESS op code +} diff --git a/libevm/pseudo/constructor.go b/libevm/pseudo/constructor.go new file mode 100644 index 000000000000..91429240340a --- /dev/null +++ b/libevm/pseudo/constructor.go @@ -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 +} diff --git a/libevm/pseudo/constructor_test.go b/libevm/pseudo/constructor_test.go new file mode 100644 index 000000000000..a28f3dd42de6 --- /dev/null +++ b/libevm/pseudo/constructor_test.go @@ -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() +} diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go new file mode 100644 index 000000000000..8c453f4cb0e7 --- /dev/null +++ b/libevm/pseudo/type.go @@ -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 +} diff --git a/libevm/pseudo/type_test.go b/libevm/pseudo/type_test.go new file mode 100644 index 000000000000..27ecf7e497ea --- /dev/null +++ b/libevm/pseudo/type_test.go @@ -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 +} diff --git a/params/config.go b/params/config.go index 21ede457fd68..2e5850c440dc 100644 --- a/params/config.go +++ b/params/config.go @@ -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" ) @@ -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. @@ -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. @@ -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), @@ -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 } diff --git a/params/config.libevm.go b/params/config.libevm.go new file mode 100644 index 000000000000..37665be21060 --- /dev/null +++ b/params/config.libevm.go @@ -0,0 +1,232 @@ +package params + +import ( + "encoding/json" + "fmt" + "math/big" + "reflect" + "runtime" + "strings" + + "github.com/ethereum/go-ethereum/libevm/pseudo" +) + +// Extras are arbitrary payloads to be added as extra fields in [ChainConfig] +// and [Rules] structs. See [RegisterExtras]. +type Extras[C ChainConfigHooks, R RulesHooks] struct { + // NewRules, if non-nil is called at the end of [ChainConfig.Rules] with the + // newly created [Rules] and other context from the method call. Its + // returned value will be the extra payload of the [Rules]. If NewRules is + // nil then so too will the [Rules] extra payload be a nil `*R`. + // + // NewRules MAY modify the [Rules] but MUST NOT modify the [ChainConfig]. + NewRules func(_ *ChainConfig, _ *Rules, _ *C, blockNum *big.Int, isMerge bool, timestamp uint64) *R +} + +// RegisterExtras registers the types `C` and `R` such that they are carried as +// extra payloads in [ChainConfig] and [Rules] structs, respectively. It is +// expected to be called in an `init()` function and MUST NOT be called more +// than once. Both `C` and `R` MUST be structs. +// +// After registration, JSON unmarshalling of a [ChainConfig] will create a new +// `*C` and unmarshal the JSON key "extra" into it. Conversely, JSON marshalling +// will populate the "extra" key with the contents of the `*C`. Both the +// [json.Marshaler] and [json.Unmarshaler] interfaces are honoured if +// implemented by `C` and/or `R.` +// +// Calls to [ChainConfig.Rules] will call the `NewRules` function of the +// registered [Extras] to create a new `*R`. +// +// The payloads can be accessed via the [ExtraPayloadGetter.FromChainConfig] and +// [ExtraPayloadGetter.FromRules] methods of the getter returned by +// RegisterExtras. Where stated in the interface definitions, they will also be +// used as hooks to alter Ethereum behaviour; if this isn't desired then they +// can embed [NOOPHooks] to satisfy either interface. +func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPayloadGetter[C, R] { + if registeredExtras != nil { + panic("re-registration of Extras") + } + mustBeStruct[C]() + mustBeStruct[R]() + + getter := e.getter() + registeredExtras = &extraConstructors{ + chainConfig: pseudo.NewConstructor[C](), + rules: pseudo.NewConstructor[R](), + newForRules: e.newForRules, + getter: getter, + } + return getter +} + +// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to +// [RegisterExtras]. It panics if called from a non-testing call stack. +// +// In tests it SHOULD be called before every call to [RegisterExtras] and then +// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is +// a workaround for the single-call limitation on [RegisterExtras]. +func TestOnlyClearRegisteredExtras() { + pc := make([]uintptr, 10) + runtime.Callers(0, pc) + frames := runtime.CallersFrames(pc) + for { + f, more := frames.Next() + if strings.Contains(f.File, "/testing/") || strings.HasSuffix(f.File, "_test.go") { + registeredExtras = nil + return + } + if !more { + panic("no _test.go file in call stack") + } + } +} + +// registeredExtras holds non-generic constructors for the [Extras] types +// registered via [RegisterExtras]. +var registeredExtras *extraConstructors + +type extraConstructors struct { + chainConfig, rules pseudo.Constructor + newForRules func(_ *ChainConfig, _ *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type + // use top-level hooksFrom() functions instead of these as they handle + // instances where no [Extras] were registered. + getter interface { + hooksFromChainConfig(*ChainConfig) ChainConfigHooks + hooksFromRules(*Rules) RulesHooks + } +} + +func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type { + if e.NewRules == nil { + return registeredExtras.rules.NilPointer() + } + rExtra := e.NewRules(c, r, e.getter().FromChainConfig(c), blockNum, isMerge, timestamp) + return pseudo.From(rExtra).Type +} + +func (*Extras[C, R]) getter() (g ExtraPayloadGetter[C, R]) { return } + +// mustBeStruct panics if `T` isn't a struct. +func mustBeStruct[T any]() { + if k := reflect.TypeFor[T]().Kind(); k != reflect.Struct { + panic(notStructMessage[T]()) + } +} + +// notStructMessage returns the message with which [mustBeStruct] might panic. +// It exists to avoid change-detector tests should the message contents change. +func notStructMessage[T any]() string { + var x T + return fmt.Sprintf("%T is not a struct", x) +} + +// An ExtraPayloadGettter provides strongly typed access to the extra payloads +// carried by [ChainConfig] and [Rules] structs. The only valid way to construct +// a getter is by a call to [RegisterExtras]. +type ExtraPayloadGetter[C ChainConfigHooks, R RulesHooks] struct { + _ struct{} // make godoc show unexported fields so nobody tries to make their own getter ;) +} + +// FromChainConfig returns the ChainConfig's extra payload. +func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C { + return pseudo.MustNewValue[*C](c.extraPayload()).Get() +} + +// hooksFromChainConfig is equivalent to FromChainConfig(), but returns an +// interface instead of the concrete type implementing it; this allows it to be +// used in non-generic code. If the concrete-type value is nil (typically +// because no [Extras] were registered) a [noopHooks] is returned so it can be +// used without nil checks. +func (e ExtraPayloadGetter[C, R]) hooksFromChainConfig(c *ChainConfig) ChainConfigHooks { + if h := e.FromChainConfig(c); h != nil { + return *h + } + return NOOPHooks{} +} + +// FromRules returns the Rules' extra payload. +func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R { + return pseudo.MustNewValue[*R](r.extraPayload()).Get() +} + +// hooksFromRules is the [RulesHooks] equivalent of hooksFromChainConfig(). +func (e ExtraPayloadGetter[C, R]) hooksFromRules(r *Rules) RulesHooks { + if h := e.FromRules(r); h != nil { + return *h + } + return NOOPHooks{} +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (c *ChainConfig) UnmarshalJSON(data []byte) error { + type raw ChainConfig // doesn't inherit methods so avoids recursing back here (infinitely) + cc := &struct { + *raw + Extra *pseudo.Type `json:"extra"` + }{ + raw: (*raw)(c), // embedded to achieve regular JSON unmarshalling + } + if e := registeredExtras; e != nil { + cc.Extra = e.chainConfig.NilPointer() // `c.extra` is otherwise unexported + } + + if err := json.Unmarshal(data, cc); err != nil { + return err + } + c.extra = cc.Extra + return nil +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (c *ChainConfig) MarshalJSON() ([]byte, error) { + // See UnmarshalJSON() for rationale. + type raw ChainConfig + cc := &struct { + *raw + Extra *pseudo.Type `json:"extra"` + }{raw: (*raw)(c), Extra: c.extra} + return json.Marshal(cc) +} + +var _ interface { + json.Marshaler + json.Unmarshaler +} = (*ChainConfig)(nil) + +// addRulesExtra is called at the end of [ChainConfig.Rules]; it exists to +// abstract the libevm-specific behaviour outside of original geth code. +func (c *ChainConfig) addRulesExtra(r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) { + r.extra = nil + if registeredExtras != nil { + r.extra = registeredExtras.newForRules(c, r, blockNum, isMerge, timestamp) + } +} + +// extraPayload returns the ChainConfig's extra payload iff [RegisterExtras] has +// already been called. If the payload hasn't been populated (typically via +// unmarshalling of JSON), a nil value is constructed and returned. +func (c *ChainConfig) extraPayload() *pseudo.Type { + if registeredExtras == nil { + // This will only happen if someone constructs an [ExtraPayloadGetter] + // directly, without a call to [RegisterExtras]. + // + // See https://google.github.io/styleguide/go/best-practices#when-to-panic + panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", c)) + } + if c.extra == nil { + c.extra = registeredExtras.chainConfig.NilPointer() + } + return c.extra +} + +// extraPayload is equivalent to [ChainConfig.extraPayload]. +func (r *Rules) extraPayload() *pseudo.Type { + if registeredExtras == nil { + // See ChainConfig.extraPayload() equivalent. + panic(fmt.Sprintf("%T.ExtraPayload() called before RegisterExtras()", r)) + } + if r.extra == nil { + r.extra = registeredExtras.rules.NilPointer() + } + return r.extra +} diff --git a/params/config.libevm_test.go b/params/config.libevm_test.go new file mode 100644 index 000000000000..9b17cb428795 --- /dev/null +++ b/params/config.libevm_test.go @@ -0,0 +1,164 @@ +package params + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/libevm/pseudo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type rawJSON struct { + json.RawMessage + NOOPHooks +} + +var _ interface { + json.Marshaler + json.Unmarshaler +} = (*rawJSON)(nil) + +func TestRegisterExtras(t *testing.T) { + type ( + ccExtraA struct { + A string `json:"a"` + ChainConfigHooks + } + rulesExtraA struct { + A string + RulesHooks + } + ccExtraB struct { + B string `json:"b"` + ChainConfigHooks + } + rulesExtraB struct { + B string + RulesHooks + } + ) + + tests := []struct { + name string + register func() + ccExtra *pseudo.Type + wantRulesExtra any + }{ + { + name: "Rules payload copied from ChainConfig payload", + register: func() { + RegisterExtras(Extras[ccExtraA, rulesExtraA]{ + NewRules: func(cc *ChainConfig, r *Rules, ex *ccExtraA, _ *big.Int, _ bool, _ uint64) *rulesExtraA { + return &rulesExtraA{ + A: ex.A, + } + }, + }) + }, + ccExtra: pseudo.From(&ccExtraA{ + A: "hello", + }).Type, + wantRulesExtra: &rulesExtraA{ + A: "hello", + }, + }, + { + name: "no NewForRules() function results in typed but nil pointer", + register: func() { + RegisterExtras(Extras[ccExtraB, rulesExtraB]{}) + }, + ccExtra: pseudo.From(&ccExtraB{ + B: "world", + }).Type, + wantRulesExtra: (*rulesExtraB)(nil), + }, + { + name: "custom JSON handling honoured", + register: func() { + RegisterExtras(Extras[rawJSON, struct{ RulesHooks }]{}) + }, + ccExtra: pseudo.From(&rawJSON{ + RawMessage: []byte(`"hello, world"`), + }).Type, + wantRulesExtra: (*struct{ RulesHooks })(nil), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + TestOnlyClearRegisteredExtras() + tt.register() + defer TestOnlyClearRegisteredExtras() + + in := &ChainConfig{ + ChainID: big.NewInt(142857), + extra: tt.ccExtra, + } + + buf, err := json.Marshal(in) + require.NoError(t, err) + + got := new(ChainConfig) + require.NoError(t, json.Unmarshal(buf, got)) + assert.Equal(t, tt.ccExtra.Interface(), got.extraPayload().Interface()) + assert.Equal(t, in, got) + // TODO: do we need an explicit test of the JSON output, or is a + // Marshal-Unmarshal round trip sufficient? + + gotRules := got.Rules(nil, false, 0) + assert.Equal(t, tt.wantRulesExtra, gotRules.extraPayload().Interface()) + }) + } +} + +func TestExtrasPanic(t *testing.T) { + TestOnlyClearRegisteredExtras() + defer TestOnlyClearRegisteredExtras() + + assertPanics( + t, func() { + new(ChainConfig).extraPayload() + }, + "before RegisterExtras", + ) + + assertPanics( + t, func() { + new(Rules).extraPayload() + }, + "before RegisterExtras", + ) + + assertPanics( + t, func() { + mustBeStruct[int]() + }, + notStructMessage[int](), + ) + + RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{}) + + assertPanics( + t, func() { + RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{}) + }, + "re-registration", + ) +} + +func assertPanics(t *testing.T, fn func(), wantContains string) { + t.Helper() + defer func() { + switch r := recover().(type) { + case nil: + t.Error("function did not panic as expected") + case string: + assert.Contains(t, r, wantContains) + default: + t.Fatalf("BAD TEST SETUP: recover() got unsupported type %T", r) + } + }() + fn() +} diff --git a/params/example.libevm_test.go b/params/example.libevm_test.go new file mode 100644 index 000000000000..37e8b2e4b580 --- /dev/null +++ b/params/example.libevm_test.go @@ -0,0 +1,158 @@ +// In practice, everything in this file except for the Example() function SHOULD +// be a standalone package, typically called `extraparams`. As long as this new +// package is imported anywhere, its init() function will register the "extra" +// types, which can be accessed via [extraparams.FromChainConfig] and/or +// [extraparams.FromRules]. In all other respects, the [params.ChainConfig] and +// [params.Rules] types will act as expected. +// +// The Example() function demonstrates how the `extraparams` package might be +// used from elsewhere. +package params_test + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/libevm" + "github.com/ethereum/go-ethereum/params" +) + +// In practice this would be a regular init() function but nuances around the +// testing of this package require it to be called in the Example(). +func initFn() { + params.TestOnlyClearRegisteredExtras() // not necessary outside of the example + // This registration makes *all* [params.ChainConfig] and [params.Rules] + // instances respect the payload types. They do not need to be modified to + // know about `extraparams`. + getter = params.RegisterExtras(params.Extras[ChainConfigExtra, RulesExtra]{ + NewRules: constructRulesExtra, + }) +} + +var getter params.ExtraPayloadGetter[ChainConfigExtra, RulesExtra] + +// constructRulesExtra acts as an adjunct to the [params.ChainConfig.Rules] +// method. Its primary purpose is to construct the extra payload for the +// [params.Rules] but it MAY also modify the [params.Rules]. +func constructRulesExtra(c *params.ChainConfig, r *params.Rules, cEx *ChainConfigExtra, blockNum *big.Int, isMerge bool, timestamp uint64) *RulesExtra { + return &RulesExtra{ + IsMyFork: cEx.MyForkTime != nil && *cEx.MyForkTime <= timestamp, + timestamp: timestamp, + } +} + +// ChainConfigExtra can be any struct. Here it just mirrors a common pattern in +// the standard [params.ChainConfig] struct. +type ChainConfigExtra struct { + MyForkTime *uint64 `json:"myForkTime"` +} + +// RulesExtra can be any struct. It too mirrors a common pattern in +// [params.Rules]. +type RulesExtra struct { + IsMyFork bool + timestamp uint64 + + // (Optional) If not all hooks are desirable then embedding a [NOOPHooks] + // allows the type to satisfy the [RulesHooks] interface, resulting in + // default Ethereum behaviour. + params.NOOPHooks +} + +// FromChainConfig returns the extra payload carried by the ChainConfig. +func FromChainConfig(c *params.ChainConfig) *ChainConfigExtra { + return getter.FromChainConfig(c) +} + +// FromRules returns the extra payload carried by the Rules. +func FromRules(r *params.Rules) *RulesExtra { + return getter.FromRules(r) +} + +// myForkPrecompiledContracts is analogous to the vm.PrecompiledContracts +// maps. Note [RulesExtra.PrecompileOverride] treatment of nil values here. +var myForkPrecompiledContracts = map[common.Address]vm.PrecompiledContract{ + //... + common.BytesToAddress([]byte{0x2}): nil, // i.e disabled + //... +} + +// PrecompileOverride implements the required [params.RuleHooks] method. +func (r RulesExtra) PrecompileOverride(addr common.Address) (_ libevm.PrecompiledContract, override bool) { + if !r.IsMyFork { + return nil, false + } + p, ok := myForkPrecompiledContracts[addr] + // The returned boolean indicates whether or not [vm.EVMInterpreter] MUST + // override the address, not what it returns as its own `isPrecompile` + // boolean. + // + // Therefore returning `nil, true` here indicates that the precompile will + // be disabled. Returning `false` here indicates that the default precompile + // behaviour will be exhibited. + // + // The same pattern can alternatively be implemented with an explicit + // `disabledPrecompiles` set to make the behaviour clearer. + return p, ok +} + +// CanCreateContract implements the required [params.RuleHooks] method. Access +// to state allows it to be configured on-chain however this is an optional +// implementation detail. +func (r RulesExtra) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error { + if time.Unix(int64(r.timestamp), 0).UTC().Day() != int(time.Tuesday) { + return errors.New("uh oh!") + } + return nil +} + +// This example demonstrates how the rest of this file would be used from a +// *different* package. +func ExampleExtraPayloadGetter() { + initFn() // Outside of an example this is unnecessary as the function will be a regular init(). + + const forkTime = 530003640 + jsonData := fmt.Sprintf(`{ + "chainId": 1234, + "extra": { + "myForkTime": %d + } + }`, forkTime) + + // Because [params.RegisterExtras] has been called, unmarshalling a JSON + // field of "extra" into a [params.ChainConfig] will populate a new value of + // the registered type. This can be accessed with the [FromChainConfig] + // function. + config := new(params.ChainConfig) + if err := json.Unmarshal([]byte(jsonData), config); err != nil { + log.Fatal(err) + } + + fmt.Println("Chain ID", config.ChainID) // original geth fields work as expected + + ccExtra := FromChainConfig(config) // extraparams.FromChainConfig() in practice + if ccExtra != nil && ccExtra.MyForkTime != nil { + fmt.Println("Fork time", *ccExtra.MyForkTime) + } + + for _, time := range []uint64{forkTime - 1, forkTime, forkTime + 1} { + rules := config.Rules(nil, false, time) + rExtra := FromRules(&rules) // extraparams.FromRules() in practice + if rExtra != nil { + fmt.Printf("IsMyFork at %v: %t\n", rExtra.timestamp, rExtra.IsMyFork) + } + } + + // Output: + // Chain ID 1234 + // Fork time 530003640 + // IsMyFork at 530003639: false + // IsMyFork at 530003640: true + // IsMyFork at 530003641: true +} diff --git a/params/hooks.libevm.go b/params/hooks.libevm.go new file mode 100644 index 000000000000..c44cd3f1f055 --- /dev/null +++ b/params/hooks.libevm.go @@ -0,0 +1,83 @@ +package params + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/libevm" +) + +// ChainConfigHooks are required for all types registered as [Extras] for +// [ChainConfig] payloads. +type ChainConfigHooks interface{} + +// TODO(arr4n): given the choice of whether a hook should be defined on a +// ChainConfig or on the Rules, what are the guiding principles? A ChainConfig +// carries the most general information while Rules benefit from "knowing" the +// block number and timestamp. I am leaning towards the default choice being +// on Rules (as it's trivial to copy information from ChainConfig to Rules in +// [Extras.NewRules]) unless the call site only has access to a ChainConfig. + +// RulesHooks are required for all types registered as [Extras] for [Rules] +// payloads. +type RulesHooks interface { + RulesAllowlistHooks + // PrecompileOverride signals whether or not the EVM interpreter MUST + // override its treatment of the address when deciding if it is a + // precompiled contract. If PrecompileOverride returns `true` then the + // interpreter will treat the address as a precompile i.f.f the + // [PrecompiledContract] is non-nil. If it returns `false` then the default + // precompile behaviour is honoured. + PrecompileOverride(common.Address) (_ libevm.PrecompiledContract, override bool) +} + +// RulesAllowlistHooks are a subset of [RulesHooks] that gate actions, signalled +// by returning a nil (allowed) or non-nil (blocked) error. +type RulesAllowlistHooks interface { + CanCreateContract(*libevm.AddressContext, libevm.StateReader) error + CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error +} + +// Hooks returns the hooks registered with [RegisterExtras], or [NOOPHooks] if +// none were registered. +func (c *ChainConfig) Hooks() ChainConfigHooks { + if e := registeredExtras; e != nil { + return e.getter.hooksFromChainConfig(c) + } + return NOOPHooks{} +} + +// Hooks returns the hooks registered with [RegisterExtras], or [NOOPHooks] if +// none were registered. +func (r *Rules) Hooks() RulesHooks { + if e := registeredExtras; e != nil { + return e.getter.hooksFromRules(r) + } + return NOOPHooks{} +} + +// NOOPHooks implements both [ChainConfigHooks] and [RulesHooks] such that every +// hook is a no-op. This allows it to be returned instead of a nil interface, +// which would otherwise require every usage site to perform a nil check. It can +// also be embedded in structs that only wish to implement a sub-set of hooks. +// Use of a NOOPHooks is equivalent to default Ethereum behaviour. +type NOOPHooks struct{} + +var _ interface { + ChainConfigHooks + RulesHooks +} = NOOPHooks{} + +// CanExecuteTransaction allows all (otherwise valid) transactions. +func (NOOPHooks) CanExecuteTransaction(_ common.Address, _ *common.Address, _ libevm.StateReader) error { + return nil +} + +// CanCreateContract allows all (otherwise valid) contract deployment. +func (NOOPHooks) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error { + return nil +} + +// PrecompileOverride instructs the EVM interpreter to use the default +// precompile behaviour. +func (NOOPHooks) PrecompileOverride(common.Address) (libevm.PrecompiledContract, bool) { + return nil, false +}