diff --git a/nimbus/config.nim b/nimbus/config.nim index 3c78677e6..5802eaf23 100644 --- a/nimbus/config.nim +++ b/nimbus/config.nim @@ -176,6 +176,12 @@ type defaultValueDesc: "" name: "verify-from" }: Option[uint64] + evm* {. + desc: "Load alternative EVM from EVMC-compatible shared library (.so/.dll/.dylib)" + defaultValue: "" + defaultValueDesc: "Nimbus built-in EVM" + name: "evm" }: string + network {. separator: "\pETHEREUM NETWORK OPTIONS:" desc: "Name or id number of Ethereum network(mainnet(1), ropsten(3), rinkeby(4), goerli(5), kovan(42), other=custom)" diff --git a/nimbus/nimbus.nim b/nimbus/nimbus.nim index 2301d2e1f..a93174c6c 100644 --- a/nimbus/nimbus.nim +++ b/nimbus/nimbus.nim @@ -20,6 +20,7 @@ import ./p2p/blockchain_sync, eth/net/nat, eth/p2p/peer_pool, ./sync/protocol_eth65, config, genesis, rpc/[common, p2p, debug], p2p/chain, + transaction/evmc_dynamic_loader, eth/trie/db, metrics, metrics/[chronos_httpserver, chronicles_support], graphql/ethapi, context, "."/[conf_utils, sealer, constants] @@ -198,6 +199,8 @@ proc start(nimbus: NimbusNode, conf: NimbusConf) = defaultChroniclesStream.output.outFile = nil # to avoid closing stdout discard defaultChroniclesStream.output.open(logFile, fmAppend) + evmcSetLibraryPath(conf.evm) + createDir(string conf.dataDir) let trieDB = trieDB newChainDb(string conf.dataDir) var chainDB = newBaseChainDB(trieDB, diff --git a/nimbus/transaction/evmc_dynamic_loader.nim b/nimbus/transaction/evmc_dynamic_loader.nim new file mode 100644 index 000000000..555b52cdb --- /dev/null +++ b/nimbus/transaction/evmc_dynamic_loader.nim @@ -0,0 +1,119 @@ +# Nimbus - Dynamic loader for EVM modules as shared libraries / DLLs +# +# 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 + std/[dynlib, strformat, strutils, os], + chronicles, + evmc/evmc, ../config + +# The built-in Nimbus EVM, via imported C function. +proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, importc.} + +# Import this module to pull in the definition of `evmc_create_nimbus_evm`. +# Nim thinks the module is unused because the function is only called via +# `.exportc` -> `.importc`. +{.warning[UnusedImport]: off.}: + import ./evmc_vm_glue + +var evmcLibraryPath {.threadvar.}: string + ## Library path set by `--evm` command line option. + ## Like the loaded EVMs themselves, this is thread-local for Nim simplicity. + +proc evmcSetLibraryPath*(path: string) = + evmcLibraryPath = path + +proc evmcLoadVMGetCreateFn(): (evmc_create_vm_name_fn, string) = + var path = evmcLibraryPath + if path.len == 0: + path = getEnv("NIMBUS_EVM") + + # Use built-in EVM if no other is specified. + if path.len == 0: + return (evmc_create_nimbus_evm, "built-in") + + # The steps below match the EVMC Loader documentation, copied here: + # + # - The filename is used to guess the EVM name and the name of the create + # function. The create function name is constructed by the following + # rules. Consider example path: "/ethereum/libexample-interpreter.so.1.0". + # - The filename is taken from the path: "libexample-interpreter.so.1.0". + # - The "lib" prefix and all file extensions are stripped from the name: + # "example-interpreter". + # - All "-" are replaced with "_" to construct base name: + # "example_interpreter" + # - The function name "evmc_create_" + base name is searched in the library: + # "evmc_create_example_interpreter", + # - If the function is not found, the function name "evmc_create" is searched + # in the library. + + # Load the library. + let lib = loadLib(path, false) + if lib.isNil: + warn "Error loading EVM library", path + return (nil, "") + + # Find filename in the path. + var symbolName = os.extractFilename(path) + # Skip "lib" prefix if present. Note, `removePrefix` only removes at the + # start despite its documentation. + symbolName.removePrefix("lib") + # Trim all file extesnsions. (`os.splitFile` only removes the last.) + symbolName = symbolName.split('.', 1)[0] + # Replace all "-" with "_". + symbolName = symbolName.replace('-', '_') + + # Search for the built function name. + symbolName = "evmc_create_" & symbolName + var sym = symAddr(lib, symbolName) + if sym.isNil: + const fallback = "evmc_create" + sym = symAddr(lib, fallback) + if sym.isNil: + warn "EVMC create function not found in library", path + warn "Tried this library symbol", symbol=symbolName + warn "Tried this library symbol", symbol=fallback + return (nil, "") + + return (cast[evmc_create_vm_name_fn](sym), path) + +proc evmcLoadVMShowDetail(): ptr evmc_vm = + let (vmCreate, vmDescription) = evmcLoadVMGetCreateFn() + if vmCreate.isNil: + return nil + + {.gcsafe.}: + let vm: ptr evmc_vm = vmCreate() + + if vm.isNil: + warn "The loaded EVM did not create a VM when requested", + `from`=vmDescription + return nil + + if vm.abi_version != EVMC_ABI_VERSION: + warn "The loaded EVM is for an incompatible EVMC ABI", + requireABI=EVMC_ABI_VERSION, loadedABI=vm.abi_version, + `from`=vmDescription + warn "The loaded EVM will not be used", `from`=vmDescription + return nil + + let name = if vm.name.isNil: "" else: $vm.name + let version = if vm.version.isNil: "" else: $vm.version + info "Using EVM", name=name, version=version, `from`=vmDescription + return vm + +proc evmcLoadVMCached*(): ptr evmc_vm = + # TODO: Make this open the VM library once per process. Currently it does + # so once per thread, but at least this is thread-safe. + var vm {.threadvar.}: ptr evmc_vm + var triedLoading {.threadvar.}: bool + if not triedLoading: + triedLoading = true + vm = evmcLoadVMShowDetail() + return vm diff --git a/nimbus/transaction/evmc_host_glue.nim b/nimbus/transaction/evmc_host_glue.nim index 150f0b91c..e2d9cb911 100644 --- a/nimbus/transaction/evmc_host_glue.nim +++ b/nimbus/transaction/evmc_host_glue.nim @@ -11,7 +11,7 @@ when not declaredInScope(included_from_host_services): {.error: "Do not import this file directly, import host_services instead".} -import evmc/evmc +import evmc/evmc, ./evmc_dynamic_loader template toHost(p: evmc_host_context): TransactionHost = cast[TransactionHost](p) @@ -94,19 +94,12 @@ let hostInterface = evmc_host_interface( access_storage: accessStorage, ) -# The built-in Nimbus EVM, via imported C function. -proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, importc.} - -# Pull in the definition of the above function because we're not building it as -# a separate library yet. -import ./evmc_vm_glue - proc evmcExecComputation*(host: TransactionHost): EvmcResult {.inline.} = host.showCallEntry(host.msg) - let vm = evmc_create_nimbus_evm() + let vm = evmcLoadVMCached() if vm.isNil: - echo "Warning: No EVM" + warn "No EVM" # Nim defaults are fine for all other fields in the result object. result = EvmcResult(status_code: EVMC_INTERNAL_ERROR) host.showCallReturn(result) @@ -138,3 +131,9 @@ proc evmcExecComputation*(host: TransactionHost): EvmcResult {.inline.} = host.code.len.csize_t) host.showCallReturn(result) + +# This code assumes fields, methods and types of ABI version 9, and must be +# checked for compatibility if the `import evmc/evmc` major version is updated. +when EVMC_ABI_VERSION != 9: + {.error: ("This code assumes EVMC_ABI_VERSION 9;" & + " update the code to use EVMC_ABI_VERSION " & $EVMC_ABI_VERSION).} diff --git a/nimbus/transaction/evmc_vm_glue.nim b/nimbus/transaction/evmc_vm_glue.nim index a90318112..ae83e4d45 100644 --- a/nimbus/transaction/evmc_vm_glue.nim +++ b/nimbus/transaction/evmc_vm_glue.nim @@ -92,7 +92,7 @@ proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, exportc.} = ## 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. + abi_version: EVMC_ABI_VERSION, name: evmcName, version: evmcVersion, destroy: evmcDestroy, @@ -103,3 +103,9 @@ proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, exportc.} = # Keep an extra reference on this, until `evmcDestroy` is called. GC_ref(vm) return cast[ptr evmc_vm](vm) + +# This code assumes fields, methods and types of ABI version 9, and must be +# checked for compatibility if the `import evmc/evmc` major version is updated. +when EVMC_ABI_VERSION != 9: + {.error: ("This code assumes EVMC_ABI_VERSION 9;" & + " update the code to use EVMC_ABI_VERSION " & $EVMC_ABI_VERSION).}