diff --git a/nimbus.nimble b/nimbus.nimble index 49ac9a37a..02e8eba96 100644 --- a/nimbus.nimble +++ b/nimbus.nimble @@ -31,7 +31,15 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = proc test(name: string, lang = "c") = buildBinary name, "tests/", "-d:chronicles_log_level=ERROR" - exec "build/" & name + when not defined(windows): + # Verify stack usage is kept low by setting 1024k stack limit in tests. + exec "ulimit -s 1024 && build/" & name + else: + # Don't enforce stack limit in Windows, as we can't control it with these tools. + # See https://public-inbox.org/git/alpine.DEB.2.21.1.1709131448390.4132@virtualbox/ + # When set by ulimit -s` in Bash, it's ignored. Also, the command passed to + # NimScript `exec` on Windows is not a shell script. + exec "build/" & name task test, "Run tests": test "all_tests" diff --git a/nimbus/vm/computation.nim b/nimbus/vm/computation.nim index 932e0f5d2..a8210f499 100644 --- a/nimbus/vm/computation.nim +++ b/nimbus/vm/computation.nim @@ -291,46 +291,61 @@ proc afterExecCreate(c: Computation) = else: c.rollback() -proc beforeExec(c: Computation): bool = +proc beforeExec(c: Computation): bool {.noinline.} = if not c.msg.isCreate: c.beforeExecCall() false else: c.beforeExecCreate() -proc afterExec(c: Computation) = +proc afterExec(c: Computation) {.noinline.} = if not c.msg.isCreate: c.afterExecCall() else: c.afterExecCreate() -template chainTo*(c, toChild: Computation, after: untyped) = +template chainTo*(c: Computation, toChild: typeof(c.child), after: untyped) = c.child = toChild c.continuation = proc() = after -proc execCallOrCreate*(cParam: Computation) = - var (c, before) = (cParam, true) - defer: - while not c.isNil: - c.dispose() - c = c.parent - - # No actual recursion, but simulate recursion including before/after/dispose. - while true: - while true: - if before and c.beforeExec(): - break +when vm_use_recursion: + # Recursion with tiny stack frame per level. + proc execCallOrCreate*(c: Computation) = + defer: c.dispose() + if c.beforeExec(): + return + c.executeOpcodes() + while not c.continuation.isNil: + when evmc_enabled: + c.res = c.host.call(c.child[]) + else: + execCallOrCreate(c.child) + c.child = nil c.executeOpcodes() - if c.continuation.isNil: - c.afterExec() + c.afterExec() + +else: + # No actual recursion, but simulate recursion including before/after/dispose. + proc execCallOrCreate*(cParam: Computation) = + var (c, before) = (cParam, true) + defer: + while not c.isNil: + c.dispose() + c = c.parent + while true: + while true: + if before and c.beforeExec(): + break + c.executeOpcodes() + if c.continuation.isNil: + c.afterExec() + break + (before, c.child, c, c.parent) = (true, nil.Computation, c.child, c) + if c.parent.isNil: break - (before, c.child, c, c.parent) = (true, nil.Computation, c.child, c) - if c.parent.isNil: - break - c.dispose() - (before, c.parent, c) = (false, nil.Computation, c.parent) - (c.continuation)() + c.dispose() + (before, c.parent, c) = (false, nil.Computation, c.parent) proc merge*(c, child: Computation) = c.logEntries.add child.logEntries diff --git a/nimbus/vm/evmc_host.nim b/nimbus/vm/evmc_host.nim index 72b66e6f1..e0c315676 100644 --- a/nimbus/vm/evmc_host.nim +++ b/nimbus/vm/evmc_host.nim @@ -123,7 +123,7 @@ proc hostEmitLogImpl(ctx: Computation, address: EthAddress, log.address = address ctx.addLogEntry(log) -template createImpl(c: Computation, m: nimbus_message, res: nimbus_result) = +proc enterCreateImpl(c: Computation, m: nimbus_message): Computation = # TODO: use evmc_message to evoid copy let childMsg = Message( kind: CallKind(m.kind), @@ -133,10 +133,9 @@ template createImpl(c: Computation, m: nimbus_message, res: nimbus_result) = value: Uint256.fromEvmc(m.value), data: @(makeOpenArray(m.inputData, m.inputSize.int)) ) + return newComputation(c.vmState, childMsg, Uint256.fromEvmc(m.create2_salt)) - let child = newComputation(c.vmState, childMsg, Uint256.fromEvmc(m.create2_salt)) - child.execCallOrCreate() - +template leaveCreateImpl(c, child: Computation, res: nimbus_result) = if not child.shouldBurnGas: res.gas_left = child.gasMeter.gasRemaining @@ -153,7 +152,7 @@ template createImpl(c: Computation, m: nimbus_message, res: nimbus_result) = copyMem(res.output_data, child.output[0].addr, child.output.len) res.release = hostReleaseResultImpl -template callImpl(c: Computation, m: nimbus_message, res: nimbus_result) = +template enterCallImpl(c: Computation, m: nimbus_message): Computation = let childMsg = Message( kind: CallKind(m.kind), depth: m.depth, @@ -165,10 +164,9 @@ template callImpl(c: Computation, m: nimbus_message, res: nimbus_result) = data: @(makeOpenArray(m.inputData, m.inputSize.int)), flags: MsgFlags(m.flags) ) + newComputation(c.vmState, childMsg) - let child = newComputation(c.vmState, childMsg) - child.execCallOrCreate() - +template leaveCallImpl(c, child: Computation, res: nimbus_result) = if not child.shouldBurnGas: res.gas_left = child.gasMeter.gasRemaining @@ -185,11 +183,22 @@ template callImpl(c: Computation, m: nimbus_message, res: nimbus_result) = copyMem(res.output_data, child.output[0].addr, child.output.len) res.release = hostReleaseResultImpl -proc hostCallImpl(ctx: Computation, msg: var nimbus_message): nimbus_result {.cdecl.} = +proc enterHostCall(c: Computation, msg: var nimbus_message): Computation {.noinline.} = if msg.kind == EVMC_CREATE or msg.kind == EVMC_CREATE2: - createImpl(ctx, msg, result) + enterCreateImpl(c, msg) else: - callImpl(ctx, msg, result) + enterCallImpl(c, msg) + +proc leaveHostCall(c, child: Computation, kind: evmc_call_kind): nimbus_result {.noinline.} = + if kind == EVMC_CREATE or kind == EVMC_CREATE2: + leaveCreateImpl(c, child, result) + else: + leaveCallImpl(c, child, result) + +proc hostCallImpl(ctx: Computation, msg: var nimbus_message): nimbus_result {.cdecl.} = + let child = enterHostCall(ctx, msg) + child.execCallOrCreate() + leaveHostCall(ctx, child, msg.kind) proc initHostInterface(): evmc_host_interface = result.account_exists = cast[evmc_account_exists_fn](hostAccountExistsImpl) diff --git a/nimbus/vm/interpreter/opcodes_impl.nim b/nimbus/vm/interpreter/opcodes_impl.nim index e8a4b15d3..b75dcc872 100644 --- a/nimbus/vm/interpreter/opcodes_impl.nim +++ b/nimbus/vm/interpreter/opcodes_impl.nim @@ -646,7 +646,8 @@ template genCreate(callName: untyped, opCode: Op): untyped = c.gasMeter.consumeGas(createMsgGas, reason="CREATE") when evmc_enabled: - let msg = nimbus_message( + let msg = new(nimbus_message) + msg[] = nimbus_message( kind: callKind.evmc_call_kind, depth: (c.msg.depth + 1).int32, gas: createMsgGas, @@ -657,16 +658,15 @@ template genCreate(callName: untyped, opCode: Op): untyped = create2_salt: toEvmc(salt) ) - var res = c.host.call(msg) - c.returnData = @(makeOpenArray(res.outputData, res.outputSize.int)) - c.gasMeter.returnGas(res.gas_left) + c.chainTo(msg): + c.returnData = @(makeOpenArray(c.res.outputData, c.res.outputSize.int)) + c.gasMeter.returnGas(c.res.gas_left) - if res.status_code == EVMC_SUCCESS: - c.stack.top(res.create_address) + if c.res.status_code == EVMC_SUCCESS: + c.stack.top(c.res.create_address) - # TODO: a good candidate for destructor - if not res.release.isNil: - res.release(res) + if not c.res.release.isNil: + c.res.release(c.res) else: let childMsg = Message( kind: callKind, @@ -829,7 +829,8 @@ template genCall(callName: untyped, opCode: Op): untyped = return when evmc_enabled: - let msg = nimbus_message( + let msg = new(nimbus_message) + msg[] = nimbus_message( kind: callKind.evmc_call_kind, depth: (c.msg.depth + 1).int32, gas: childGasLimit, @@ -841,22 +842,21 @@ template genCall(callName: untyped, opCode: Op): untyped = flags: flags.uint32 ) - var res = c.host.call(msg) - c.returnData = @(makeOpenArray(res.outputData, res.outputSize.int)) + c.chainTo(msg): + c.returnData = @(makeOpenArray(c.res.outputData, c.res.outputSize.int)) - let actualOutputSize = min(memOutLen, c.returnData.len) - if actualOutputSize > 0: - c.memory.write(memOutPos, - c.returnData.toOpenArray(0, actualOutputSize - 1)) + let actualOutputSize = min(memOutLen, c.returnData.len) + if actualOutputSize > 0: + c.memory.write(memOutPos, + c.returnData.toOpenArray(0, actualOutputSize - 1)) - c.gasMeter.returnGas(res.gas_left) + c.gasMeter.returnGas(c.res.gas_left) - if res.status_code == EVMC_SUCCESS: - c.stack.top(1) + if c.res.status_code == EVMC_SUCCESS: + c.stack.top(1) - # TODO: a good candidate for destructor - if not res.release.isNil: - res.release(res) + if not c.res.release.isNil: + c.res.release(c.res) else: let msg = Message( kind: callKind, diff --git a/nimbus/vm/interpreter_dispatch.nim b/nimbus/vm/interpreter_dispatch.nim index 593d76dc1..f2a2f0710 100644 --- a/nimbus/vm/interpreter_dispatch.nim +++ b/nimbus/vm/interpreter_dispatch.nim @@ -387,6 +387,7 @@ proc executeOpcodes(c: Computation) = block: if not c.continuation.isNil: + (c.continuation)() c.continuation = nil elif c.execPrecompiles(fork): break diff --git a/nimbus/vm/types.nim b/nimbus/vm/types.nim index a1aa9faf6..63ac66fcf 100644 --- a/nimbus/vm/types.nim +++ b/nimbus/vm/types.nim @@ -20,6 +20,11 @@ when defined(evmc_enabled): import ./evmc_api +# Select between small-stack recursion and no recursion. Both are good, fast, +# low resource using methods. Keep both here because true EVMC API requires +# the small-stack method, but Chronos `async` is better without recursion. +const vm_use_recursion* = defined(evmc_enabled) + type VMFlag* = enum ExecutionOK @@ -85,7 +90,11 @@ type savePoint*: SavePoint instr*: Op opIndex*: int - parent*, child*: Computation + when defined(evmc_enabled): + child*: ref nimbus_message + res*: nimbus_result + else: + parent*, child*: Computation continuation*: proc() {.gcsafe.} Error* = ref object