EVMC: Binary compatibility on the VM side for calling `execute`

This provides the functions a loadable VM must provide for a host to use it.
The main access to a loaded EVM is `evmc_create_nimbus_evm`, and this meant to
be the only exported function the caller starts with.

That provides access to other functions, also defined in this patch, to
configure the EVM and then the key interesting function is `execute`.

`execute` runs a full computation, here using Nimbus EVM `Computation`.

(Note, even though everything is EVMC binary-compatible, there is a small
dependency on `TransactionHost` in `execute` here, which prevents this being
used by a host that is not Nimbus at the moment.  It is necessary for some
tests, and will eventually go away.)

Although this provides the VM-side functionality needed by the host, it does
not contain the glue functions for `Computation` to call the host, which are
already part of the Nimbus EVM in `nimbus/vm/evmc_api.nim`.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
This commit is contained in:
Jamie Lokier 2021-05-21 06:24:09 +01:00
parent 0b19f42158
commit 10807bce39
No known key found for this signature in database
GPG Key ID: CBC25C68435C30A2
1 changed files with 105 additions and 0 deletions

View File

@ -0,0 +1,105 @@
# Nimbus - Binary compatibility on the VM side of the EVMC API interface
#
# Copyright (c) 2019-2021 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except according to those terms.
#{.push raises: [Defect].}
import
./host_types, evmc/evmc, stew/ranges/ptr_arith,
".."/[vm_types, vm_computation, vm_state_transactions]
proc evmcReleaseResult(result: var evmc_result) {.cdecl.} =
dealloc(result.output_data)
proc evmcExecute(vm: ptr evmc_vm, hostInterface: ptr evmc_host_interface,
hostContext: evmc_host_context, rev: evmc_revision,
msg: var evmc_message, code: ptr byte,
code_size: csize_t): evmc_result {.cdecl.} =
# TODO: Obviously we are cheating here at the moment, knowing the caller type.
# TODO: This lets the host read extra results needed for tests, but it
# means the Nimbus EVM cannot be used by a non-Nimbus host, yet.
let host = cast[TransactionHost](hostContext)
var c = host.computation
# Allocate `Computation` on demand, and leave it in `host` for that to
# extract additional results.
#if c.isNil:
# let cMsg = hostToComputationMessage(host.msg)
# # TODO: Can we avoid `seq[byte]` so we don't have to copy the code?
# let codeSeq = if code_size <= 0: @[]
# else: @(makeOpenArray(code, code_size.int))
# c = newComputation(host.vmState, cMsg, codeSeq)
# c.host.init(cast[ptr nimbus_host_interface](hostInterface), hostContext)
# host.computation = c
c.host.init(cast[ptr nimbus_host_interface](hostInterface), hostContext)
try:
execComputation(c)
except Exception as e:
c.setError("Caught error " & e.repr, true);
# When output size is zero, output data pointer may be null.
var output_data: ptr byte
var output_size: int
if c.output.len > 0:
# TODO: Don't use a copy, share the underlying data and use `GC_ref`.
# Return a copy, because reference counting is complicated across the
# shared library boundary. We could use a refcount but we don't have a
# `ref seq[byte]` to start with, so need to check `GC_ref` on a non-ref
# `seq` does what we want first.
output_size = c.output.len
# The `alloc` here matches `dealloc` in `evmcReleaseResult`.
output_data = cast[ptr byte](alloc(output_size))
copyMem(output_data, c.output[0].addr, output_size)
return evmc_result(
# Standard EVMC result, if a bit generic.
status_code: if c.isSuccess: EVMC_SUCCESS
elif not c.error.burnsGas: EVMC_REVERT
else: EVMC_FAILURE,
# Gas left is required to be zero when not `EVMC_SUCCESS` or `EVMC_REVERT`.
gas_left: if result.status_code notin {EVMC_SUCCESS, EVMC_REVERT}: 0'i64
else: c.gasMeter.gasRemaining.int64,
output_data: output_data,
output_size: output_size.csize_t,
release: if output_data.isNil: nil
else: evmcReleaseResult
# Nim defaults are fine for `create_address` and `padding`, zero bytes.
)
when defined(vm2_enabled):
const evmcName = "Nimbus EVM (vm2)"
else:
const evmcName = "Nimbus EVM (vm1)"
const evmcVersion = "0.0.1"
proc evmcGetCapabilities(vm: ptr evmc_vm): evmc_capabilities {.cdecl.} =
{EVMC_CAPABILITY_EVM1, EVMC_CAPABILITY_PRECOMPILES}
proc evmcSetOption(vm: ptr evmc_vm, name, value: cstring): evmc_set_option_result {.cdecl.} =
return EVMC_SET_OPTION_INVALID_NAME
proc evmcDestroy(vm: ptr evmc_vm) {.cdecl.} =
GC_unref(cast[ref evmc_vm](vm))
proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, exportc.} =
## Entry point to the Nimbus EVM, using an EVMC compatible interface.
## This is an exported C function. EVMC specifies the function must
## have this name format when exported from a shared library.
let vm = (ref evmc_vm)(
abi_version: 7, # Not 8, we don't support ABI version 8 yet.
name: evmcName,
version: evmcVersion,
destroy: evmcDestroy,
execute: evmcExecute,
get_capabilities: evmcGetCapabilities,
set_option: evmcSetOption
)
# Keep an extra reference on this, until `evmcDestroy` is called.
GC_ref(vm)
return cast[ptr evmc_vm](vm)