Skip to content

Commit

Permalink
feat(vm): EVM active precompiles (#7)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladislav Varadinov <vladislav.varadinov@gmail.com>
  • Loading branch information
fedekunze and Vvaradinov authored Jan 16, 2023
1 parent 01f8889 commit e61d503
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Improvements

* [#7](https://github.com/evmos/go-ethereum/pull/7) Implement custom active precompiles for the EVM.
* [#3](https://github.com/evmos/go-ethereum/pull/3) Move the `JumpTable` defaults to a separate function.
* [#2](https://github.com/evmos/go-ethereum/pull/2) Define `Interpreter` interface for the EVM.
2 changes: 1 addition & 1 deletion core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
st.state.Prepare(rules, msg.From(), st.evm.Context.Coinbase, msg.To(), vm.ActivePrecompiles(rules), msg.AccessList())
st.state.Prepare(rules, msg.From(), st.evm.Context.Coinbase, msg.To(), st.evm.ActivePrecompiles(rules), msg.AccessList())

var (
ret []byte
Expand Down
92 changes: 89 additions & 3 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package vm

import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -127,8 +129,8 @@ func init() {
}
}

// ActivePrecompiles returns the precompiles enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address {
// DefaultActivePrecompiles returns the set of precompiles enabled with the default configuration.
func DefaultActivePrecompiles(rules params.Rules) []common.Address {
switch {
case rules.IsBerlin:
return PrecompiledAddressesBerlin
Expand All @@ -141,6 +143,87 @@ func ActivePrecompiles(rules params.Rules) []common.Address {
}
}

// DefaultPrecompiles define the mapping of address and precompiles from the default configuration
func DefaultPrecompiles(rules params.Rules) (precompiles map[common.Address]PrecompiledContract) {
switch {
case rules.IsBerlin:
precompiles = PrecompiledContractsBerlin
case rules.IsIstanbul:
precompiles = PrecompiledContractsIstanbul
case rules.IsByzantium:
precompiles = PrecompiledContractsByzantium
default:
precompiles = PrecompiledContractsHomestead
}

return precompiles
}

// ActivePrecompiles returns the precompiles enabled with the current configuration.
//
// NOTE: The rules argument is ignored as the active precompiles can be set via the WithPrecompiles
// method according to the chain rules from the current block context.
func (evm *EVM) ActivePrecompiles(_ params.Rules) []common.Address {
return evm.activePrecompiles
}

// Precompile returns a precompiled contract for the given address. This
// function returns false if the address is not a registered precompile.
func (evm *EVM) Precompile(addr common.Address) (PrecompiledContract, bool) {
p, ok := evm.precompiles[addr]
return p, ok
}

// WithPrecompiles sets the precompiled contracts and the slice of actives precompiles.
// IMPORTANT: This function does NOT validate the precompiles provided to the EVM. The caller should
// use the ValidatePrecompiles function for this purpose prior to calling WithPrecompiles.
func (evm *EVM) WithPrecompiles(
precompiles map[common.Address]PrecompiledContract,
activePrecompiles []common.Address,
) {
evm.precompiles = precompiles
evm.activePrecompiles = activePrecompiles
}

// ValidatePrecompiles validates the precompile map against the active
// precompile slice.
// It returns an error if the precompiled contract map has a different length
// than the slice of active contract addresses. This function also checks for
// duplicates, invalid addresses and empty precompile contract instances.
func ValidatePrecompiles(
precompiles map[common.Address]PrecompiledContract,
activePrecompiles []common.Address,
) error {
if len(precompiles) != len(activePrecompiles) {
return fmt.Errorf("precompiles length mismatch (expected %d, got %d)", len(precompiles), len(activePrecompiles))
}

dupActivePrecompiles := make(map[common.Address]bool)

for _, addr := range activePrecompiles {
if dupActivePrecompiles[addr] {
return fmt.Errorf("duplicate active precompile: %s", addr)
}

precompile, ok := precompiles[addr]
if !ok {
return fmt.Errorf("active precompile address doesn't exist in precompiles map: %s", addr)
}

if precompile == nil {
return fmt.Errorf("precompile contract cannot be nil: %s", addr)
}

if bytes.Equal(addr.Bytes(), common.Address{}.Bytes()) {
return fmt.Errorf("precompile cannot be the zero address: %s", addr)
}

dupActivePrecompiles[addr] = true
}

return nil
}

// RunPrecompiledContract runs and evaluates the output of a precompiled contract.
// It returns
// - the returned bytes,
Expand Down Expand Up @@ -204,6 +287,7 @@ type sha256hash struct{}
func (c *sha256hash) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.Sha256PerWordGas + params.Sha256BaseGas
}

func (c *sha256hash) Run(input []byte) ([]byte, error) {
h := sha256.Sum256(input)
return h[:], nil
Expand All @@ -219,6 +303,7 @@ type ripemd160hash struct{}
func (c *ripemd160hash) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.Ripemd160PerWordGas + params.Ripemd160BaseGas
}

func (c *ripemd160hash) Run(input []byte) ([]byte, error) {
ripemd := ripemd160.New()
ripemd.Write(input)
Expand All @@ -235,6 +320,7 @@ type dataCopy struct{}
func (c *dataCopy) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.IdentityPerWordGas + params.IdentityBaseGas
}

func (c *dataCopy) Run(in []byte) ([]byte, error) {
return common.CopyBytes(in), nil
}
Expand Down Expand Up @@ -388,7 +474,7 @@ func (c *bigModExp) Run(input []byte) ([]byte, error) {
// Modulo 0 is undefined, return zero
return common.LeftPadBytes([]byte{}, int(modLen)), nil
case base.BitLen() == 1: // a bit length of 1 means it's 1 (or -1).
//If base == 1, then we can just return base % mod (if mod >= 1, which it is)
// If base == 1, then we can just return base % mod (if mod >= 1, which it is)
v = base.Mod(base, mod).Bytes()
default:
v = base.Exp(base, exp, mod).Bytes()
Expand Down
32 changes: 12 additions & 20 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,6 @@ type (
GetHashFunc func(uint64) common.Hash
)

func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
var precompiles map[common.Address]PrecompiledContract
switch {
case evm.chainRules.IsBerlin:
precompiles = PrecompiledContractsBerlin
case evm.chainRules.IsIstanbul:
precompiles = PrecompiledContractsIstanbul
case evm.chainRules.IsByzantium:
precompiles = PrecompiledContractsByzantium
default:
precompiles = PrecompiledContractsHomestead
}
p, ok := precompiles[addr]
return p, ok
}

// BlockContext provides the EVM with auxiliary information. Once provided
// it shouldn't be modified.
type BlockContext struct {
Expand Down Expand Up @@ -120,6 +104,10 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
// precompiles defines the precompiled contracts used by the EVM
precompiles map[common.Address]PrecompiledContract
// activePrecompiles defines the precompiles that are currently active
activePrecompiles []common.Address
}

// NewEVM returns a new EVM. The returned EVM is not thread safe and should
Expand All @@ -134,7 +122,11 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
}

// set the default precompiles
evm.activePrecompiles = DefaultActivePrecompiles(evm.chainRules)
evm.precompiles = DefaultPrecompiles(evm.chainRules)
evm.interpreter = NewEVMInterpreter(evm, config)

return evm
}

Expand Down Expand Up @@ -180,7 +172,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
return nil, gas, ErrInsufficientBalance
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
p, isPrecompile := evm.Precompile(addr)

if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP158 && value.Sign() == 0 {
Expand Down Expand Up @@ -279,7 +271,7 @@ 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 {
if p, isPrecompile := evm.Precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
Expand Down Expand Up @@ -320,7 +312,7 @@ 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 {
if p, isPrecompile := evm.Precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
addrCopy := addr
Expand Down Expand Up @@ -369,7 +361,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}(gas)
}

if p, isPrecompile := evm.precompile(addr); isPrecompile {
if p, isPrecompile := evm.Precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// At this point, we use a copy of address. If we don't, the go compiler will
Expand Down
6 changes: 3 additions & 3 deletions core/vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.DefaultActivePrecompiles(rules), nil)

cfg.State.CreateAccount(address)
// set the receiver's (the executing contract) code for execution.
Expand Down Expand Up @@ -156,7 +156,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.DefaultActivePrecompiles(rules), nil)

// Call the code with the given configuration.
code, address, leftOverGas, err := vmenv.Create(
Expand Down Expand Up @@ -185,7 +185,7 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.DefaultActivePrecompiles(rules), nil)

// Call the code with the given configuration.
ret, leftOverGas, err := vmenv.Call(
Expand Down
10 changes: 6 additions & 4 deletions eth/tracers/js/goja.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ func init() {
// hex strings into big ints.
var bigIntProgram = goja.MustCompile("bigInt", bigIntegerJS, false)

type toBigFn = func(vm *goja.Runtime, val string) (goja.Value, error)
type toBufFn = func(vm *goja.Runtime, val []byte) (goja.Value, error)
type fromBufFn = func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error)
type (
toBigFn = func(vm *goja.Runtime, val string) (goja.Value, error)
toBufFn = func(vm *goja.Runtime, val []byte) (goja.Value, error)
fromBufFn = func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error)
)

func toBuf(vm *goja.Runtime, bufType goja.Value, val []byte) (goja.Value, error) {
// bufType is usually Uint8Array. This is equivalent to `new Uint8Array(val)` in JS.
Expand Down Expand Up @@ -250,7 +252,7 @@ func (t *jsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Addr
t.ctx["block"] = t.vm.ToValue(env.Context.BlockNumber.Uint64())
// Update list of precompiles based on current block
rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil, env.Context.Time)
t.activePrecompiles = vm.ActivePrecompiles(rules)
t.activePrecompiles = env.ActivePrecompiles(rules)
}

// CaptureState implements the Tracer interface to trace a single step of VM execution.
Expand Down
2 changes: 1 addition & 1 deletion eth/tracers/native/4byte.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (t *fourByteTracer) store(id []byte, size int) {
func (t *fourByteTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
// Update list of precompiles based on current block
rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil, env.Context.Time)
t.activePrecompiles = vm.ActivePrecompiles(rules)
t.activePrecompiles = env.ActivePrecompiles(rules)

// Save the outer calldata also
if len(input) >= 4 {
Expand Down
9 changes: 4 additions & 5 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func (s *TxPoolAPI) Inspect() map[string]map[string]map[string]string {
pending, queue := s.b.TxPoolContent()

// Define a formatter to flatten a transaction into a string
var format = func(tx *types.Transaction) string {
format := func(tx *types.Transaction) string {
if to := tx.To(); to != nil {
return fmt.Sprintf("%s: %v wei + %v gas × %v wei", tx.To().Hex(), tx.Value(), tx.Gas(), tx.GasPrice())
}
Expand Down Expand Up @@ -1145,7 +1145,6 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
for lo+1 < hi {
mid := (hi + lo) / 2
failed, _, err := executable(mid)

// If the error is not nil(consensus error), it means the provided message
// call or transaction will never be accepted no matter how much gas it is
// assigned. Return the error directly, don't struggle any more.
Expand Down Expand Up @@ -1444,7 +1443,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
}
isPostMerge := header.Difficulty.Cmp(common.Big0) == 0
// Retrieve the precompiles since they don't need to be added to the access list
precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, isPostMerge, new(big.Int).SetUint64(header.Time)))
precompiles := vm.DefaultActivePrecompiles(b.ChainConfig().Rules(header.Number, isPostMerge, new(big.Int).SetUint64(header.Time)))

// Create an initial tracer
prevTracer := logger.NewAccessListTracer(nil, args.from(), to, precompiles)
Expand Down Expand Up @@ -1871,11 +1870,11 @@ func (s *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, g
matchTx := sendArgs.toTransaction()

// Before replacing the old transaction, ensure the _new_ transaction fee is reasonable.
var price = matchTx.GasPrice()
price := matchTx.GasPrice()
if gasPrice != nil {
price = gasPrice.ToInt()
}
var gas = matchTx.Gas()
gas := matchTx.Gas()
if gasLimit != nil {
gas = uint64(*gasLimit)
}
Expand Down
4 changes: 2 additions & 2 deletions tests/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.Error(err)
return
}
var rules = config.Rules(new(big.Int), false, new(big.Int))
rules := config.Rules(new(big.Int), false, new(big.Int))

vmconfig.ExtraEips = eips
block := t.genesis(config).ToBlock()
Expand Down Expand Up @@ -239,7 +239,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
snapshot := statedb.Snapshot()
statedb.Prepare(rules, msg.From(), context.Coinbase, msg.To(), vm.ActivePrecompiles(rules), msg.AccessList())
statedb.Prepare(rules, msg.From(), context.Coinbase, msg.To(), evm.ActivePrecompiles(rules), msg.AccessList())
b.StartTimer()
start := time.Now()

Expand Down

0 comments on commit e61d503

Please sign in to comment.