nimbus-eth1/nimbus/evm/interpreter_dispatch.nim

321 lines
10 KiB
Nim

# Nimbus
# Copyright (c) 2018-2023 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.
const
# help with low memory when compiling selectVM() function
lowmem {.intdefine.}: int = 0
lowMemoryCompileTime {.used.} = lowmem > 0
import
std/[macros, sets, strformat],
pkg/[chronicles, chronos, stew/byteutils],
".."/[constants, utils/utils, db/accounts_cache],
"."/[code_stream, computation],
"."/[message, precompiles, state, types],
./interpreter/[op_dispatcher, gas_costs]
{.push raises: [].}
logScope:
topics = "vm opcode"
const
ripemdAddr = block:
proc initAddress(x: int): EthAddress {.compileTime.} =
result[19] = x.byte
initAddress(3)
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc selectVM(c: Computation, fork: EVMFork, shouldPrepareTracer: bool)
{.gcsafe, raises: [CatchableError].} =
## Op code execution handler main loop.
var desc: Vm2Ctx
desc.cpt = c
# It's important not to re-prepare the tracer after
# an async operation, only after a call/create.
#
# That is, tracingEnabled is checked in many places, and
# indicates something like, "Do we want tracing to be
# enabled?", whereas shouldPrepareTracer is more like,
# "Are we at a spot right now where we want to re-initialize
# the tracer?"
if c.tracingEnabled and shouldPrepareTracer:
c.prepareTracer()
while true:
c.instr = c.code.next()
# Note Mamy's observation in opTableToCaseStmt() from original VM
# regarding computed goto
#
# ackn:
# #{.computedGoto.}
# # computed goto causing stack overflow, it consumes a lot of space
# # we could use manual jump table instead
# # TODO lots of macro magic here to unravel, with chronicles...
# # `c`.logger.log($`c`.stack & "\n\n", fgGreen)
when not lowMemoryCompileTime:
when defined(release):
#
# FIXME: OS case list below needs to be adjusted
#
when defined(windows):
when defined(cpu64):
{.warning: "*** Win64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
# computedGoto not compiling on github/ci (out of memory) -- jordan
{.warning: "*** Win32/VM2 handler switch => optimisation disabled".}
# {.computedGoto, optimization: speed.}
elif defined(linux):
when defined(cpu64):
{.warning: "*** Linux64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
{.warning: "*** Linux32/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
elif defined(macosx):
when defined(cpu64):
{.warning: "*** MacOs64/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
{.warning: "*** MacOs32/VM2 handler switch => computedGoto".}
{.computedGoto, optimization: speed.}
else:
{.warning: "*** Unsupported OS => no handler switch optimisation".}
genOptimisedDispatcher(fork, c.instr, desc)
else:
{.warning: "*** low memory compiler mode => program will be slow".}
genLowMemDispatcher(fork, c.instr, desc)
proc beforeExecCall(c: Computation) =
c.snapshot()
if c.msg.kind == evmcCall:
c.vmState.mutateStateDB:
db.subBalance(c.msg.sender, c.msg.value)
db.addBalance(c.msg.contractAddress, c.msg.value)
proc afterExecCall(c: Computation) =
## Collect all of the accounts that *may* need to be deleted based on EIP161
## https://github.com/ethereum/EIPs/blob/master/EIPS/eip-161.md
## also see: https://github.com/ethereum/EIPs/issues/716
if c.isError or c.fork >= FkByzantium:
if c.msg.contractAddress == ripemdAddr:
# Special case to account for geth+parity bug
c.vmState.touchedAccounts.incl c.msg.contractAddress
if c.isSuccess:
c.commit()
c.touchedAccounts.incl c.msg.contractAddress
else:
c.rollback()
proc beforeExecCreate(c: Computation): bool
{.gcsafe, raises: [ValueError].} =
c.vmState.mutateStateDB:
let nonce = db.getNonce(c.msg.sender)
if nonce+1 < nonce:
c.setError(&"Nonce overflow when sender={c.msg.sender.toHex} wants to create contract", false)
return true
db.setNonce(c.msg.sender, nonce+1)
# We add this to the access list _before_ taking a snapshot.
# Even if the creation fails, the access-list change should not be rolled
# back EIP2929
if c.fork >= FkBerlin:
db.accessList(c.msg.contractAddress)
c.snapshot()
if c.vmState.readOnlyStateDB().hasCodeOrNonce(c.msg.contractAddress):
# let blurb = c.msg.contractAddress.toHex -- notused
c.setError("Address collision when creating contract address={blurb}", true)
c.rollback()
return true
c.vmState.mutateStateDB:
db.subBalance(c.msg.sender, c.msg.value)
db.addBalance(c.msg.contractAddress, c.msg.value)
db.clearStorage(c.msg.contractAddress)
if c.fork >= FkSpurious:
# EIP161 nonce incrementation
db.incNonce(c.msg.contractAddress)
return false
proc afterExecCreate(c: Computation)
{.gcsafe, raises: [CatchableError].} =
if c.isSuccess:
# This can change `c.isSuccess`.
c.writeContract()
# Contract code should never be returned to the caller. Only data from
# `REVERT` is returned after a create. Clearing in this branch covers the
# right cases, particularly important with EVMC where it must be cleared.
if c.output.len > 0:
c.output = @[]
if c.isSuccess:
c.commit()
else:
c.rollback()
proc beforeExec(c: Computation): bool
{.gcsafe, raises: [ValueError].} =
if not c.msg.isCreate:
c.beforeExecCall()
false
else:
c.beforeExecCreate()
proc afterExec(c: Computation)
{.gcsafe, raises: [CatchableError].} =
if not c.msg.isCreate:
c.afterExecCall()
else:
c.afterExecCreate()
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc executeOpcodes*(c: Computation, shouldPrepareTracer: bool = true)
{.gcsafe, raises: [CatchableError].} =
let fork = c.fork
block:
if c.continuation.isNil and c.execPrecompiles(fork):
break
try:
if not c.continuation.isNil:
(c.continuation)()
c.selectVM(fork, shouldPrepareTracer)
except CatchableError as e:
let
msg = e.msg
depth = $c.msg.depth
c.setError("Opcode Dispatch Error msg=" & msg & ", depth=" & depth, true)
if c.isError() and c.continuation.isNil:
if c.tracingEnabled: c.traceError()
#trace "executeOpcodes error", msg=c.error.info
when vm_use_recursion:
# Recursion with tiny stack frame per level.
proc execCallOrCreate*(c: Computation)
{.gcsafe, raises: [CatchableError].} =
defer: c.dispose()
if c.beforeExec():
return
c.executeOpcodes()
while not c.continuation.isNil:
# If there's a continuation, then it's because there's either
# a child (i.e. call or create) or a pendingAsyncOperation.
if not c.pendingAsyncOperation.isNil:
let p = c.pendingAsyncOperation
c.pendingAsyncOperation = nil
doAssert(p.finished(), "In synchronous mode, every async operation should be an already-resolved Future.")
c.executeOpcodes(false)
else:
when evmc_enabled:
c.res = c.host.call(c.child[])
else:
execCallOrCreate(c.child)
c.child = nil
c.executeOpcodes()
c.afterExec()
else:
proc execCallOrCreate*(cParam: Computation)
{.gcsafe, raises: [CatchableError].} =
var (c, before, shouldPrepareTracer) = (cParam, true, 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
c.executeOpcodes(shouldPrepareTracer)
if c.continuation.isNil:
c.afterExec()
break
if not c.pendingAsyncOperation.isNil:
before = false
shouldPrepareTracer = false
let p = c.pendingAsyncOperation
c.pendingAsyncOperation = nil
doAssert(p.finished(), "In synchronous mode, every async operation should be an already-resolved Future.")
else:
(before, shouldPrepareTracer, c.child, c, c.parent) = (true, true, nil.Computation, c.child, c)
if c.parent.isNil:
break
c.dispose()
(before, shouldPrepareTracer, c.parent, c) = (false, true, nil.Computation, c.parent)
# FIXME-duplicatedForAsync
#
# In the long run I'd like to make some clever macro/template to
# eliminate the duplication between the synchronous and
# asynchronous versions. But for now let's stick with this for
# simplicity.
#
# Also, I've based this on the recursive one (above), which I think
# is okay because the "async" pragma is going to rewrite this whole
# thing to use callbacks anyway. But maybe I'm wrong? It isn't hard
# to write the async version of the iterative one, but this one is
# a bit shorter and feels cleaner, so if it works just as well I'd
# rather use this one. --Adam
proc asyncExecCallOrCreate*(c: Computation): Future[void] {.async.} =
defer: c.dispose()
if c.beforeExec():
return
c.executeOpcodes()
while not c.continuation.isNil:
# If there's a continuation, then it's because there's either
# a child (i.e. call or create) or a pendingAsyncOperation.
if not c.pendingAsyncOperation.isNil:
let p = c.pendingAsyncOperation
c.pendingAsyncOperation = nil
await p
c.executeOpcodes(false)
else:
when evmc_enabled:
# FIXME-asyncAndEvmc
# Note that this is NOT async. I'm not sure how/whether I
# can do EVMC asynchronously.
c.res = c.host.call(c.child[])
else:
await asyncExecCallOrCreate(c.child)
c.child = nil
c.executeOpcodes()
c.afterExec()
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------