Skip to content

Commit

Permalink
chore: squash arr4n/libevm into libevm (#7)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ARR4N authored Sep 10, 2024
1 parent 2bd6bd0 commit 5429fd8
Show file tree
Hide file tree
Showing 22 changed files with 1,516 additions and 7 deletions.
3 changes: 3 additions & 0 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down
9 changes: 9 additions & 0 deletions core/state_transition.libevm.go
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions core/state_transition.libevm_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
4 changes: 2 additions & 2 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
83 changes: 83 additions & 0 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
@@ -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 changes: 178 additions & 0 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}
Loading

0 comments on commit 5429fd8

Please # to comment.