mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
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
This commit is contained in:
parent
2bd6bd01d2
commit
5429fd87c8
22 changed files with 1516 additions and 7 deletions
|
|
@ -365,6 +365,9 @@ func (st *StateTransition) preCheck() error {
|
||||||
// However if any consensus issue encountered, return the error directly with
|
// However if any consensus issue encountered, return the error directly with
|
||||||
// nil evm execution result.
|
// nil evm execution result.
|
||||||
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
||||||
|
if err := st.canExecuteTransaction(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// First check this message satisfies all consensus rules before
|
// First check this message satisfies all consensus rules before
|
||||||
// applying the message. The rules include these clauses
|
// applying the message. The rules include these clauses
|
||||||
//
|
//
|
||||||
|
|
|
||||||
9
core/state_transition.libevm.go
Normal file
9
core/state_transition.libevm.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
40
core/state_transition.libevm_test.go
Normal file
40
core/state_transition.libevm_test.go
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -168,13 +168,13 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
|
||||||
// - the returned bytes,
|
// - the returned bytes,
|
||||||
// - the _remaining_ gas,
|
// - the _remaining_ gas,
|
||||||
// - any error that occurred
|
// - 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)
|
gasCost := p.RequiredGas(input)
|
||||||
if suppliedGas < gasCost {
|
if suppliedGas < gasCost {
|
||||||
return nil, 0, ErrOutOfGas
|
return nil, 0, ErrOutOfGas
|
||||||
}
|
}
|
||||||
suppliedGas -= gasCost
|
suppliedGas -= gasCost
|
||||||
output, err := p.Run(input)
|
output, err := args.run(p, input)
|
||||||
return output, suppliedGas, err
|
return output, suppliedGas, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
83
core/vm/contracts.libevm.go
Normal file
83
core/vm/contracts.libevm.go
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
||||||
178
core/vm/contracts.libevm_test.go
Normal file
178
core/vm/contracts.libevm_test.go
Normal file
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/libevm"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
@ -38,6 +39,9 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
|
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
|
var precompiles map[common.Address]PrecompiledContract
|
||||||
switch {
|
switch {
|
||||||
case evm.chainRules.IsCancun:
|
case evm.chainRules.IsCancun:
|
||||||
|
|
@ -224,7 +228,8 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
|
||||||
}
|
}
|
||||||
|
|
||||||
if isPrecompile {
|
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 {
|
} else {
|
||||||
// Initialise a new contract and set the code that is to be used by the EVM.
|
// 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.
|
// 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
|
// It is allowed to call precompiles, even via delegatecall
|
||||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
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 {
|
} else {
|
||||||
addrCopy := addr
|
addrCopy := addr
|
||||||
// Initialise a new contract and set the code that is to be used by the EVM.
|
// 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
|
// It is allowed to call precompiles, even via delegatecall
|
||||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
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 {
|
} else {
|
||||||
addrCopy := addr
|
addrCopy := addr
|
||||||
// Initialise a new contract and make initialise the delegate values
|
// 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 {
|
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 {
|
} else {
|
||||||
// At this point, we use a copy of address. If we don't, the go compiler will
|
// 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'
|
// 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.
|
// 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) {
|
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
|
// Depth check execution. Fail if we're trying to execute above the
|
||||||
// limit.
|
// limit.
|
||||||
if evm.depth > int(params.CallCreateDepth) {
|
if evm.depth > int(params.CallCreateDepth) {
|
||||||
|
|
|
||||||
7
core/vm/libevm_test.go
Normal file
7
core/vm/libevm_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
37
libevm/ethtest/evm.go
Normal file
37
libevm/ethtest/evm.go
Normal file
|
|
@ -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{},
|
||||||
|
)
|
||||||
|
}
|
||||||
41
libevm/ethtest/rand.go
Normal file
41
libevm/ethtest/rand.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
60
libevm/hookstest/stub.go
Normal file
60
libevm/hookstest/stub.go
Normal file
|
|
@ -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{}
|
||||||
20
libevm/interfaces_test.go
Normal file
20
libevm/interfaces_test.go
Normal file
|
|
@ -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)
|
||||||
52
libevm/libevm.go
Normal file
52
libevm/libevm.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
24
libevm/pseudo/constructor.go
Normal file
24
libevm/pseudo/constructor.go
Normal file
|
|
@ -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
libevm/pseudo/constructor_test.go
Normal file
45
libevm/pseudo/constructor_test.go
Normal file
|
|
@ -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
libevm/pseudo/type.go
Normal file
175
libevm/pseudo/type.go
Normal file
|
|
@ -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
libevm/pseudo/type_test.go
Normal file
79
libevm/pseudo/type_test.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/libevm/pseudo"
|
||||||
"github.com/ethereum/go-ethereum/params/forks"
|
"github.com/ethereum/go-ethereum/params/forks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -365,6 +366,8 @@ type ChainConfig struct {
|
||||||
// Various consensus engines
|
// Various consensus engines
|
||||||
Ethash *EthashConfig `json:"ethash,omitempty"`
|
Ethash *EthashConfig `json:"ethash,omitempty"`
|
||||||
Clique *CliqueConfig `json:"clique,omitempty"`
|
Clique *CliqueConfig `json:"clique,omitempty"`
|
||||||
|
|
||||||
|
extra *pseudo.Type // See RegisterExtras()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
|
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
|
||||||
|
|
@ -902,6 +905,8 @@ type Rules struct {
|
||||||
IsBerlin, IsLondon bool
|
IsBerlin, IsLondon bool
|
||||||
IsMerge, IsShanghai, IsCancun, IsPrague bool
|
IsMerge, IsShanghai, IsCancun, IsPrague bool
|
||||||
IsVerkle bool
|
IsVerkle bool
|
||||||
|
|
||||||
|
extra *pseudo.Type // See RegisterExtras()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rules ensures c's ChainID is not nil.
|
// 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
|
// disallow setting Merge out of order
|
||||||
isMerge = isMerge && c.IsLondon(num)
|
isMerge = isMerge && c.IsLondon(num)
|
||||||
return Rules{
|
r := Rules{
|
||||||
ChainID: new(big.Int).Set(chainID),
|
ChainID: new(big.Int).Set(chainID),
|
||||||
IsHomestead: c.IsHomestead(num),
|
IsHomestead: c.IsHomestead(num),
|
||||||
IsEIP150: c.IsEIP150(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),
|
IsPrague: isMerge && c.IsPrague(num, timestamp),
|
||||||
IsVerkle: isMerge && c.IsVerkle(num, timestamp),
|
IsVerkle: isMerge && c.IsVerkle(num, timestamp),
|
||||||
}
|
}
|
||||||
|
c.addRulesExtra(&r, num, isMerge, timestamp)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
|
||||||
232
params/config.libevm.go
Normal file
232
params/config.libevm.go
Normal file
|
|
@ -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<X>() 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
|
||||||
|
}
|
||||||
164
params/config.libevm_test.go
Normal file
164
params/config.libevm_test.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
158
params/example.libevm_test.go
Normal file
158
params/example.libevm_test.go
Normal file
|
|
@ -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<Fork>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
83
params/hooks.libevm.go
Normal file
83
params/hooks.libevm.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue