nimbus-eth1/nimbus/transaction/host_call_nested.nim

163 lines
6.8 KiB
Nim

# Nimbus - Services available to EVM code that is run for a transaction
#
# Copyright (c) 2019-2024 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: [].}
import
eth/common/eth_types,
stew/ptrops,
stew/saturation_arith,
stint,
../evm/types,
../evm/interpreter_dispatch,
../utils/utils,
"."/[host_types, host_trace]
import ../evm/computation except fromEvmc, toEvmc
proc evmcResultRelease(res: var EvmcResult) {.cdecl, gcsafe.} =
dealloc(res.output_data)
proc beforeExecCreateEvmcNested(host: TransactionHost,
m: EvmcMessage): Computation =
# TODO: use evmc_message to avoid copy
let childMsg = Message(
kind: CallKind(m.kind.ord),
depth: m.depth,
gas: GasInt m.gas,
sender: m.sender.fromEvmc,
value: m.value.fromEvmc,
data: @(makeOpenArray(m.input_data, m.input_size.int))
)
return newComputation(host.vmState, false, childMsg,
cast[ContractSalt](m.create2_salt))
proc afterExecCreateEvmcNested(host: TransactionHost, child: Computation,
res: var EvmcResult) {.inline.} =
if not child.shouldBurnGas:
res.gas_left = int64.saturate(child.gasMeter.gasRemaining)
if child.isSuccess:
res.gas_refund = child.gasMeter.gasRefunded
res.status_code = EVMC_SUCCESS
res.create_address = child.msg.contractAddress.toEvmc
else:
res.status_code = child.evmcStatus
if child.output.len > 0:
# TODO: can we move the ownership of seq to raw pointer?
res.output_size = child.output.len.uint
res.output_data = cast[ptr byte](alloc(child.output.len))
copyMem(res.output_data, child.output[0].addr, child.output.len)
res.release = evmcResultRelease
proc beforeExecCallEvmcNested(host: TransactionHost,
m: EvmcMessage): Computation {.inline.} =
let childMsg = Message(
kind: CallKind(m.kind.ord),
depth: m.depth,
gas: GasInt m.gas,
sender: m.sender.fromEvmc,
codeAddress: m.code_address.fromEvmc,
contractAddress: if m.kind == EVMC_CALL:
m.recipient.fromEvmc
else:
host.computation.msg.contractAddress,
value: m.value.fromEvmc,
data: @(makeOpenArray(m.input_data, m.input_size.int)),
flags: m.flags,
)
return newComputation(host.vmState, false, childMsg)
proc afterExecCallEvmcNested(host: TransactionHost, child: Computation,
res: var EvmcResult) {.inline.} =
if not child.shouldBurnGas:
res.gas_left = int64.saturate(child.gasMeter.gasRemaining)
if child.isSuccess:
res.gas_refund = child.gasMeter.gasRefunded
res.status_code = EVMC_SUCCESS
else:
res.status_code = child.evmcStatus
if child.output.len > 0:
# TODO: can we move the ownership of seq to raw pointer?
res.output_size = child.output.len.uint
res.output_data = cast[ptr byte](alloc(child.output.len))
copyMem(res.output_data, child.output[0].addr, child.output.len)
res.release = evmcResultRelease
# The next three functions are designed so `callEvmcNested` uses very small C
# stack usage for each level of nested EVM calls.
#
# To keep the C stack usage small when there are deeply nested EVM calls,
# `callEvmcNested` must use as little stack as possible, going from the EVM
# which calls it to the nested EVM which it calls.
#
# First, `callEvmcNested` itself is `template` so it is inlined to the caller
# at Nim level, not C level. Only at Nim level is inlining guaranteed across
# `import`. This saves a C stack frame, which matters because some C compilers
# reserve space for 1-3 copies of the large `EvmcResult` return value.
#
# Second, the complicated parts of preparation and return are done in
# out-of-line functions `beforeExecEvmcNested` and `afterExecEvmcNested`. They
# are annotated with `{.noinline.}` to make sure they are out-of-line. The
# annotation ensures they don't contribute to the stack frame of
# `callEvmcNested`, because otherwise the compiler can optimistically inline.
# (Even across modules when using `-flto`).
#
# The functions `beforeExecEvmcNested` and `afterExecEvmcNested` can use as
# much stack as they like.
proc beforeExecEvmcNested(host: TransactionHost, msg: EvmcMessage): Computation
# This function must be declared with `{.noinline.}` to make sure it doesn't
# contribute to the stack frame of `callEvmcNested` below.
{.noinline.} =
# `call` is special. Most host functions do `flip256` in `evmc_host_glue`
# and `show` in `host_services`, but `call` needs to minimise C stack used
# by nested EVM calls. Just `flip256` in glue's `call` adds a lot of
# stack: +65% in tests, enough to blow our 750kiB test stack target and
# crash. Easily avoided by doing `flip256` and `show` out-of-line here.
var msg = msg # Make a local copy that's ok to modify.
msg.value = flip256(msg.value)
host.showCallEntry(msg)
let c = if msg.kind == EVMC_CREATE or msg.kind == EVMC_CREATE2:
beforeExecCreateEvmcNested(host, msg)
else:
beforeExecCallEvmcNested(host, msg)
when defined(evmc_enabled):
c.host.init(cast[ptr nimbus_host_interface](host.hostInterface),
cast[typeof(c.host.context)](host))
host.saveComputation.add(host.computation)
host.computation = c
return c
proc afterExecEvmcNested(host: TransactionHost, child: Computation,
kind: EvmcCallKind): EvmcResult
# This function must be declared with `{.noinline.}` to make sure it doesn't
# contribute to the stack frame of `callEvmcNested` below.
{.noinline.} =
host.computation = host.saveComputation[^1]
host.saveComputation[^1] = nil
host.saveComputation.setLen(host.saveComputation.len - 1)
if kind == EVMC_CREATE or kind == EVMC_CREATE2:
afterExecCreateEvmcNested(host, child, result)
else:
afterExecCallEvmcNested(host, child, result)
host.showCallReturn(result, kind.isCreate)
template callEvmcNested*(host: TransactionHost, msg: EvmcMessage): EvmcResult =
# `call` is special. The C stack usage must be kept small for deeply nested
# EVM calls. To ensure small stack, this function must use `template` to
# inline at Nim level (same for `host.call(msg)`). `{.inline.}` is not good
# enough. Due to object return it ends up using a lot more stack. (Note
# that template parameters `host` and `msg` are multiple-evaluated here;
# simple expressions must be used when calling.)
let child = beforeExecEvmcNested(host, msg)
child.execCallOrCreate()
afterExecEvmcNested(host, child, msg.kind)