From 0b19f4215843926a63a6864d873b17b922116e39 Mon Sep 17 00:00:00 2001 From: Jamie Lokier Date: Thu, 20 May 2021 10:15:00 +0100 Subject: [PATCH] EVMC: Binary compatibility glue on the host side 1. This provides the necessary type adjustments for host services to be (optionally) callable via the EVMC binary-compatible interface. This layer is stashed away in a glue module so the host services continue to use appropriate Nim types, and are still callable directly. Inlining is used to ensure there should be no real overhead, including stack frame size for the `call` function. Note, `import` must be used for `{.inline.}` to work effectively. 2. This also provides a key call in the other direction, the version of host to EVM `execute` that is called on the host side. Signed-off-by: Jamie Lokier --- nimbus/transaction/evmc_host_glue.nim | 119 ++++++++++++++++++++++++++ nimbus/transaction/host_services.nim | 21 ++++- 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 nimbus/transaction/evmc_host_glue.nim diff --git a/nimbus/transaction/evmc_host_glue.nim b/nimbus/transaction/evmc_host_glue.nim new file mode 100644 index 000000000..72c84531b --- /dev/null +++ b/nimbus/transaction/evmc_host_glue.nim @@ -0,0 +1,119 @@ +# Nimbus - Binary compatibility on the host 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].} + +when not declaredInScope(included_from_host_services): + {.error: "Do not import this file directly, import host_services instead".} + +import evmc/evmc + +template toHost(p: evmc_host_context): TransactionHost = + cast[TransactionHost](p) + +proc accountExists(p: evmc_host_context, address: var evmc_address): c99bool {.cdecl.} = + toHost(p).accountExists(address.fromEvmc) + +proc getStorage(p: evmc_host_context, address: var evmc_address, + key: var evmc_bytes32): evmc_bytes32 {.cdecl.} = + toHost(p).getStorage(address.fromEvmc, key.fromEvmc).toEvmc + +proc setStorage(p: evmc_host_context, address: var evmc_address, + key, value: var evmc_bytes32): evmc_storage_status {.cdecl.} = + toHost(p).setStorage(address.fromEvmc, key.fromEvmc, value.fromEvmc) + +proc getBalance(p: evmc_host_context, + address: var evmc_address): evmc_uint256be {.cdecl.} = + toHost(p).getBalance(address.fromEvmc).toEvmc + +proc getCodeSize(p: evmc_host_context, + address: var evmc_address): csize_t {.cdecl.} = + toHost(p).getCodeSize(address.fromEvmc) + +proc getCodeHash(p: evmc_host_context, + address: var evmc_address): evmc_bytes32 {.cdecl.} = + toHost(p).getCodeHash(address.fromEvmc).toEvmc + +proc copyCode(p: evmc_host_context, address: var evmc_address, code_offset: csize_t, + buffer_data: ptr byte, buffer_size: csize_t): csize_t {.cdecl.} = + toHost(p).copyCode(address.fromEvmc, code_offset, buffer_data, buffer_size) + +proc selfDestruct(p: evmc_host_context, address, + beneficiary: var evmc_address) {.cdecl.} = + toHost(p).selfDestruct(address.fromEvmc, beneficiary.fromEvmc) + +proc call(p: evmc_host_context, msg: var evmc_message): evmc_result {.cdecl.} = + toHost(p).call(msg) + +proc getTxContext(p: evmc_host_context): evmc_tx_context {.cdecl.} = + toHost(p).getTxContext() + +proc getBlockHash(p: evmc_host_context, number: int64): evmc_bytes32 {.cdecl.} = + # TODO: `HostBlockNumber` is 256-bit unsigned. It should be changed to match + # EVMC which is more sensible. + toHost(p).getBlockHash(number.uint64.u256).toEvmc + +proc emitLog(p: evmc_host_context, address: var evmc_address, + data: ptr byte, data_size: csize_t, + topics: ptr evmc_bytes32, topics_count: csize_t) {.cdecl.} = + toHost(p).emitLog(address.fromEvmc, data, data_size, + cast[ptr HostTopic](topics), topics_count) + +proc evmcGetHostInterface(): ref evmc_host_interface = + var theHostInterface {.global, threadvar.}: ref evmc_host_interface + if theHostInterface.isNil: + theHostInterface = (ref evmc_host_interface)( + account_exists: accountExists, + get_storage: getStorage, + set_storage: setStorage, + get_balance: getBalance, + get_code_size: getCodeSize, + get_code_hash: getCodeHash, + copy_code: copyCode, + selfdestruct: selfDestruct, + call: call, + get_tx_context: getTxContext, + get_block_hash: getBlockHash, + emit_log: emitLog, + ) + return theHostInterface + +# The built-in Nimbus EVM, via imported C function. +proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, importc.} + +proc evmcExecComputation*(host: TransactionHost, code: seq[byte]): EvmcResult = + let vm = evmc_create_nimbus_evm() + if vm.isNil: + echo "Warning: No EVM" + # Nim defaults are fine for all other fields in the result object. + return EvmcResult(status_code: EVMC_INTERNAL_ERROR) + + let hostInterface = evmcGetHostInterface() + let hostContext = cast[evmc_host_context](host) + + # Without `{.gcsafe.}:` here, the call via `vm.execute` results in a Nim + # compile-time error in a far away function. Starting here, a cascade of + # warnings takes place: "Warning: '...' is not GC-safe as it performs an + # indirect call here [GCUnsafe2]", then a list of "Warning: '...' is not + # GC-safe as it calls '...'" at each function up the call stack, to a high + # level function `persistBlocks` where it terminates compilation as an error + # instead of a warning. + # + # It is tempting to annotate all EVMC API functions with `{.cdecl, gcsafe.}`, + # overriding the function signatures from the Nim EVMC module. Perhaps we + # will do that, though it's conceptually dubious, as the two sides of the + # EVMC ABI live in different GC worlds (when loaded as a shared library with + # its own Nim runtime), very similar to calling between threads. + # + # TODO: But wait: Why does the Nim EVMC test program compile fine without + # any `gcsafe`, even with `--threads:on`? + {.gcsafe.}: + vm.execute(vm, hostInterface[].addr, hostContext, + evmc_revision(host.vmState.fork), host.msg, + if code.len > 0: code[0].unsafeAddr else: nil, + code.len.csize_t) diff --git a/nimbus/transaction/host_services.nim b/nimbus/transaction/host_services.nim index 432046c2b..727db2375 100644 --- a/nimbus/transaction/host_services.nim +++ b/nimbus/transaction/host_services.nim @@ -64,6 +64,16 @@ proc setupTxContext(host: TransactionHost) = host.txContext.block_difficulty = vmState.blockHeader.difficulty.toEvmc host.txContext.chain_id = vmState.chaindb.config.chainId.uint.u256.toEvmc +const use_evmc_glue = defined(evmc_enabled) + +# When using the EVMC binary interface, each of the functions below is wrapped +# in another function that converts types to be compatible with the binary +# interface, and the functions below are not called directly. The conversions +# mostly just cast between byte-compatible types, so to avoid a redundant call +# layer, make the functions below `{.inline.}` when wrapped in this way. +when use_evmc_glue: + {.push inline.} + proc accountExists(host: TransactionHost, address: HostAddress): bool = if host.vmState.fork >= FkSpurious: not host.vmState.readOnlyStateDB.isDeadAccount(address) @@ -207,6 +217,11 @@ proc emitLog(host: TransactionHost, address: HostAddress, log.address = address host.logEntries.add(log) -export - accountExists, getStorage, storage, getBalance, getCodeSize, getCodeHash, - copyCode, selfDestruct, getTxContext, call, getBlockHash, emitLog +when use_evmc_glue: + {.pop: inline.} + const included_from_host_services = true + include ./evmc_host_glue +else: + export + accountExists, getStorage, storage, getBalance, getCodeSize, getCodeHash, + copyCode, selfDestruct, getTxContext, call, getBlockHash, emitLog