Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

EVMC support #17050

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package vm

import (
"math/big"
"os"
"sync/atomic"
"time"

Expand Down Expand Up @@ -155,10 +156,13 @@ func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmCon
panic("No supported ewasm interpreter yet.")
}

if len(vmConfig.EVMInterpreter) != 0 || len(os.Getenv("EVMC_PATH")) != 0 {
evm.interpreters = append(evm.interpreters, NewEVMC(vmConfig.EVMInterpreter, evm))
}

// vmConfig.EVMInterpreter will be used by EVM-C, it won't be checked here
// as we always want to have the built-in EVM as the failover option.
evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig))
evm.interpreter = evm.interpreters[0]

return evm
}
Expand Down
312 changes: 312 additions & 0 deletions core/vm/evmc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package vm

import (
"fmt"
"math/big"
"os"
"strings"
"sync"

"github.com/ethereum/evmc/bindings/go/evmc"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)

type EVMC struct {
chfast marked this conversation as resolved.
Show resolved Hide resolved
instance *evmc.Instance
env *EVM
readOnly bool // TODO: The readOnly flag should not be here.
}

var (
createMu sync.Mutex
evmcInstance *evmc.Instance
)

func createVM(path string) *evmc.Instance {
createMu.Lock()
defer createMu.Unlock()

if evmcInstance == nil {
vmPath := os.Getenv("EVMC_PATH")
if len(vmPath) == 0 {
vmPath = path
}
if len(vmPath) == 0 {
panic("EVMC VM path not provided, set EVMC_PATH environment variable or --vm.evm option")
}

var err error
evmcInstance, err = evmc.Load(vmPath)
if err != nil {
panic(err.Error())
}
log.Info("EVMC VM loaded", "name", evmcInstance.Name(), "version", evmcInstance.Version(), "path", vmPath)

for _, option := range strings.Split(os.Getenv("EVMC_OPTIONS"), " ") {
if idx := strings.Index(option, "="); idx >= 0 {
name := option[:idx]
value := option[idx+1:]
err := evmcInstance.SetOption(name, value)
if err == nil {
log.Info("EVMC VM option set", "name", name, "value", value)
} else {
log.Warn("EVMC VM option setting failed", "name", name, "error", err)
}
}
}
}
return evmcInstance
}

func NewEVMC(path string, env *EVM) *EVMC {
return &EVMC{createVM(path), env, false}
}

// Implements evmc.HostContext interface.
type HostContext struct {
env *EVM
contract *Contract
}

func (host *HostContext) AccountExists(addr common.Address) bool {
env := host.env
eip158 := env.ChainConfig().IsEIP158(env.BlockNumber)
if eip158 {
if !env.StateDB.Empty(addr) {
return true
}
} else if env.StateDB.Exist(addr) {
return true
}
return false
}

func (host *HostContext) GetStorage(addr common.Address, key common.Hash) common.Hash {
env := host.env
return env.StateDB.GetState(addr, key)
}

func (host *HostContext) SetStorage(addr common.Address, key common.Hash, value common.Hash) (status evmc.StorageStatus) {
env := host.env

oldValue := env.StateDB.GetState(addr, key)
if oldValue == value {
return evmc.StorageUnchanged
}

env.StateDB.SetState(addr, key, value)

zero := common.Hash{}
status = evmc.StorageModified
if oldValue == zero {
return evmc.StorageAdded
} else if value == zero {
env.StateDB.AddRefund(params.SstoreRefundGas)
return evmc.StorageDeleted
}
return evmc.StorageModified
}

func (host *HostContext) GetBalance(addr common.Address) common.Hash {
chfast marked this conversation as resolved.
Show resolved Hide resolved
env := host.env
balance := env.StateDB.GetBalance(addr)
return common.BigToHash(balance)
}

func (host *HostContext) GetCodeSize(addr common.Address) int {
env := host.env
return env.StateDB.GetCodeSize(addr)
}

func (host *HostContext) GetCodeHash(addr common.Address) common.Hash {
env := host.env
return env.StateDB.GetCodeHash(addr)
}

func (host *HostContext) GetCode(addr common.Address) []byte {
env := host.env
return env.StateDB.GetCode(addr)
}

func (host *HostContext) Selfdestruct(addr common.Address, beneficiary common.Address) {
env := host.env
db := env.StateDB
if !db.HasSuicided(addr) {
db.AddRefund(params.SuicideRefundGas)
}
balance := db.GetBalance(addr)
db.AddBalance(beneficiary, balance)
db.Suicide(addr)
}

func (host *HostContext) GetTxContext() (gasPrice common.Hash, origin common.Address, coinbase common.Address,
number int64, timestamp int64, gasLimit int64, difficulty common.Hash) {

env := host.env
gasPrice = common.BigToHash(env.GasPrice)
origin = env.Origin
coinbase = env.Coinbase
number = env.BlockNumber.Int64()
timestamp = env.Time.Int64()
gasLimit = int64(env.GasLimit)
difficulty = common.BigToHash(env.Difficulty)

return gasPrice, origin, coinbase, number, timestamp, gasLimit, difficulty
}

func (host *HostContext) GetBlockHash(number int64) common.Hash {
env := host.env
b := env.BlockNumber.Int64()
if number >= (b-256) && number < b {
return env.GetHash(uint64(number))
}
return common.Hash{}
}

func (host *HostContext) EmitLog(addr common.Address, topics []common.Hash, data []byte) {
env := host.env
env.StateDB.AddLog(&types.Log{
Address: addr,
Topics: topics,
Data: data,
BlockNumber: env.BlockNumber.Uint64(),
})
}

func (host *HostContext) Call(kind evmc.CallKind,
destination common.Address, sender common.Address, value *big.Int, input []byte, gas int64, depth int,
static bool) (output []byte, gasLeft int64, createAddr common.Address, err error) {

env := host.env

gasU := uint64(gas)
var gasLeftU uint64

switch kind {
case evmc.Call:
if static {
output, gasLeftU, err = env.StaticCall(host.contract, destination, input, gasU)
} else {
output, gasLeftU, err = env.Call(host.contract, destination, input, gasU, value)
}
case evmc.DelegateCall:
output, gasLeftU, err = env.DelegateCall(host.contract, destination, input, gasU)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any of these panic if static is set or is it handled up in the interface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static is magically handled inside, but this seems to be bad design. Probably should be changed later on to be handled internally in any VM.

case evmc.CallCode:
output, gasLeftU, err = env.CallCode(host.contract, destination, input, gasU, value)
case evmc.Create:
var createOutput []byte
createOutput, createAddr, gasLeftU, err = env.Create(host.contract, input, gasU, value)
isHomestead := env.ChainConfig().IsHomestead(env.BlockNumber)
if !isHomestead && err == ErrCodeStoreOutOfGas {
err = nil
}
if err == errExecutionReverted {
// Assign return buffer from REVERT.
// TODO: Bad API design: return data buffer and the code is returned in the same place. In worst case
// the code is returned also when there is not enough funds to deploy the code.
output = createOutput
}
}

// Map errors.
if err == errExecutionReverted {
err = evmc.Revert
} else if err != nil {
err = evmc.Failure
}

gasLeft = int64(gasLeftU)
return output, gasLeft, createAddr, err
}

func getRevision(env *EVM) evmc.Revision {
n := env.BlockNumber
conf := env.ChainConfig()
if conf.IsConstantinople(n) {
return evmc.Constantinople
}
if conf.IsByzantium(n) {
return evmc.Byzantium
}
if conf.IsEIP158(n) {
return evmc.SpuriousDragon
}
if conf.IsEIP150(n) {
return evmc.TangerineWhistle
}
if conf.IsHomestead(n) {
return evmc.Homestead
}
return evmc.Frontier
}

func (evm *EVMC) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
evm.env.depth++
defer func() { evm.env.depth-- }()

// Don't bother with the execution if there's no code.
if len(contract.Code) == 0 {
return nil, nil
}

kind := evmc.Call
if evm.env.StateDB.GetCodeSize(contract.Address()) == 0 {
// Guess if this is a CREATE.
kind = evmc.Create
}

// Make sure the readOnly is only set if we aren't in readOnly yet.
// This makes also sure that the readOnly flag isn't removed for child calls.
if readOnly && !evm.readOnly {
evm.readOnly = true
defer func() { evm.readOnly = false }()
}

output, gasLeft, err := evm.instance.Execute(
&HostContext{evm.env, contract},
getRevision(evm.env),
contract.Address(),
contract.Caller(),
common.BigToHash(contract.value),
input,
crypto.Keccak256Hash(contract.Code),
int64(contract.Gas),
evm.env.depth-1,
kind,
evm.readOnly,
contract.Code)

contract.Gas = uint64(gasLeft)

if err == evmc.Revert {
err = errExecutionReverted
} else if evmcError, ok := err.(evmc.Error); ok && evmcError.IsInternalError() {
panic(fmt.Sprintf("EVMC VM internal error: %s", evmcError.Error()))
}

return output, err
}

func (evm *EVMC) CanRun([]byte) bool {
return true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two issues here:

  • The EVMC will always try to execute the contract. There could be another interpreter that supports a format that the EVMC doesn't, and the native Go interpreter should be the default one in any case.
  • In the case of a mixed network that supports EVM+EWASM contracts, you need to have an external switch to be able to execute hera in mode evm1mode=fallback as discussed here, because right now it will always try to execute everything as WASM and that behavior is controlled by something outside of geth, which is horrible usability-wise.

So determining the proper format should be more advanced in any case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it should be false and then never run? EVMC is not for ewasm only, I'm using it with C++ EVM interpreter. I believe if an user specifies the execution as geth --vm myvm.so he really wants to use myvm.so.

You should rethink this CanRun() feature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the feature has been thought through enough, thank you. If you have geth --vm myvm.so you are stuck with only one interpreter, whereas this supports a mixed chain with different formats.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be handled with EVMC 6.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then in EVMC-6, and provided the right command line switch, the return value here should be true. Until then, it should check if the format is wasm and, if not, return false.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would break geth + aleth-interpreter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please be more specific. How would that break geth + aleth ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need geth + aleth-interpreter for testing. Aleth is EVM and if we check for wasm bytecode here it will be skip and the execution fallback to Go Interpreter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I'm not aiming to merge this PR now. We should definitely wait for EVMC 6 which should be soon.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let's do that. In the mean time, here is the PR that implements all the command line switches: #17687

}
Loading