300 lines
11 KiB
Nim
300 lines
11 KiB
Nim
# Nimbus - Common entry point to the EVM from all different callers
|
|
#
|
|
# Copyright (c) 2018-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: [].}
|
|
|
|
import
|
|
eth/common/eth_types, stint, options, stew/ptrops,
|
|
chronos,
|
|
".."/[vm_types, vm_state, vm_computation, vm_state_transactions],
|
|
".."/[vm_internals, vm_precompiles, vm_gas_costs],
|
|
".."/[db/accounts_cache],
|
|
../evm/async/operations,
|
|
../common/evmforks,
|
|
./host_types
|
|
|
|
when defined(evmc_enabled):
|
|
import ../utils/utils
|
|
import ./host_services
|
|
|
|
type
|
|
# Standard call parameters.
|
|
CallParams* = object
|
|
vmState*: BaseVMState # Chain, database, state, block, fork.
|
|
forkOverride*: Option[EVMFork] # Default fork is usually correct.
|
|
origin*: Option[HostAddress] # Default origin is `sender`.
|
|
gasPrice*: GasInt # Gas price for this call.
|
|
gasLimit*: GasInt # Maximum gas available for this call.
|
|
sender*: HostAddress # Sender account.
|
|
to*: HostAddress # Recipient (ignored when `isCreate`).
|
|
isCreate*: bool # True if this is a contract creation.
|
|
value*: HostValue # Value sent from sender to recipient.
|
|
input*: seq[byte] # Input data.
|
|
accessList*: AccessList # EIP-2930 (Berlin) tx access list.
|
|
noIntrinsic*: bool # Don't charge intrinsic gas.
|
|
noAccessList*: bool # Don't initialise EIP-2929 access list.
|
|
noGasCharge*: bool # Don't charge sender account for gas.
|
|
noRefund*: bool # Don't apply gas refund/burn rule.
|
|
|
|
# Standard call result. (Some fields are beyond what EVMC can return,
|
|
# and must only be used from tests because they will not always be set).
|
|
CallResult* = object
|
|
isError*: bool # True if the call failed.
|
|
gasUsed*: GasInt # Gas used by the call.
|
|
contractAddress*: EthAddress # Created account (when `isCreate`).
|
|
output*: seq[byte] # Output data.
|
|
logEntries*: seq[Log] # Output logs.
|
|
stack*: Stack # EVM stack on return (for test only).
|
|
memory*: Memory # EVM memory on return (for test only).
|
|
|
|
proc hostToComputationMessage*(msg: EvmcMessage): Message =
|
|
Message(
|
|
kind: CallKind(msg.kind.ord),
|
|
depth: msg.depth,
|
|
gas: msg.gas,
|
|
sender: msg.sender.fromEvmc,
|
|
contractAddress: msg.recipient.fromEvmc,
|
|
codeAddress: msg.code_address.fromEvmc,
|
|
value: msg.value.fromEvmc,
|
|
# When input size is zero, input data pointer may be null.
|
|
data: if msg.input_size <= 0: @[]
|
|
else: @(makeOpenArray(msg.input_data, msg.input_size.int)),
|
|
flags: if msg.isStatic: emvcStatic else: emvcNoFlags
|
|
)
|
|
|
|
func intrinsicGas*(call: CallParams, fork: EVMFork): GasInt {.inline.} =
|
|
# Compute the baseline gas cost for this transaction. This is the amount
|
|
# of gas needed to send this transaction (but that is not actually used
|
|
# for computation).
|
|
var gas = gasFees[fork][GasTransaction]
|
|
|
|
# EIP-2 (Homestead) extra intrinsic gas for contract creations.
|
|
if call.isCreate:
|
|
gas += gasFees[fork][GasTXCreate]
|
|
if fork >= FkShanghai:
|
|
gas += (gasFees[fork][GasInitcodeWord] * call.input.len.wordCount)
|
|
|
|
# Input data cost, reduced in EIP-2028 (Istanbul).
|
|
let gasZero = gasFees[fork][GasTXDataZero]
|
|
let gasNonZero = gasFees[fork][GasTXDataNonZero]
|
|
for b in call.input:
|
|
gas += (if b == 0: gasZero else: gasNonZero)
|
|
|
|
# EIP-2930 (Berlin) intrinsic gas for transaction access list.
|
|
if fork >= FkBerlin:
|
|
for account in call.accessList:
|
|
gas += ACCESS_LIST_ADDRESS_COST
|
|
gas += account.storageKeys.len * ACCESS_LIST_STORAGE_KEY_COST
|
|
return gas
|
|
|
|
proc initialAccessListEIP2929(call: CallParams) =
|
|
# EIP2929 initial access list.
|
|
let vmState = call.vmState
|
|
if vmState.fork < FkBerlin:
|
|
return
|
|
|
|
vmState.mutateStateDB:
|
|
db.accessList(call.sender)
|
|
# For contract creations the EVM will add the contract address to the
|
|
# access list itself, after calculating the new contract address.
|
|
if not call.isCreate:
|
|
db.accessList(call.to)
|
|
|
|
# EIP3651 adds coinbase to the list of addresses that should start warm.
|
|
if vmState.fork >= FkShanghai:
|
|
db.accessList(vmState.coinbase)
|
|
|
|
# TODO: Check this only adds the correct subset of precompiles.
|
|
for c in activePrecompiles():
|
|
db.accessList(c)
|
|
|
|
# EIP2930 optional access list.
|
|
for account in call.accessList:
|
|
db.accessList(account.address)
|
|
for key in account.storageKeys:
|
|
db.accessList(account.address, UInt256.fromBytesBE(key))
|
|
|
|
proc setupHost(call: CallParams): TransactionHost =
|
|
let vmState = call.vmState
|
|
vmState.setupTxContext(
|
|
origin = call.origin.get(call.sender),
|
|
gasPrice = call.gasPrice,
|
|
forkOverride = call.forkOverride
|
|
)
|
|
|
|
var intrinsicGas: GasInt = 0
|
|
if not call.noIntrinsic:
|
|
intrinsicGas = intrinsicGas(call, vmState.fork)
|
|
|
|
let host = TransactionHost(
|
|
vmState: vmState,
|
|
msg: EvmcMessage(
|
|
kind: if call.isCreate: EVMC_CREATE else: EVMC_CALL,
|
|
# Default: flags: {},
|
|
# Default: depth: 0,
|
|
gas: call.gasLimit - intrinsicGas,
|
|
recipient: call.to.toEvmc,
|
|
code_address: call.to.toEvmc,
|
|
sender: call.sender.toEvmc,
|
|
value: call.value.toEvmc,
|
|
)
|
|
# All other defaults in `TransactionHost` are fine.
|
|
)
|
|
|
|
# Generate new contract address, prepare code, and update message `recipient`
|
|
# with the contract address. This differs from the previous Nimbus EVM API.
|
|
# Guarded under `evmc_enabled` for now so it doesn't break vm2.
|
|
when defined(evmc_enabled):
|
|
var code: seq[byte]
|
|
if call.isCreate:
|
|
let sender = call.sender
|
|
let contractAddress =
|
|
generateAddress(sender, call.vmState.readOnlyStateDB.getNonce(sender))
|
|
host.msg.recipient = contractAddress.toEvmc
|
|
host.msg.input_size = 0
|
|
host.msg.input_data = nil
|
|
code = call.input
|
|
else:
|
|
# TODO: Share the underlying data, but only after checking this does not
|
|
# cause problems with the database.
|
|
code = host.vmState.readOnlyStateDB.getCode(host.msg.code_address.fromEvmc)
|
|
if call.input.len > 0:
|
|
host.msg.input_size = call.input.len.csize_t
|
|
# Must copy the data so the `host.msg.input_data` pointer
|
|
# remains valid after the end of `call` lifetime.
|
|
host.input = call.input
|
|
host.msg.input_data = host.input[0].addr
|
|
|
|
let cMsg = hostToComputationMessage(host.msg)
|
|
host.computation = newComputation(vmState, cMsg, code)
|
|
|
|
shallowCopy(host.code, code)
|
|
|
|
else:
|
|
if call.input.len > 0:
|
|
host.msg.input_size = call.input.len.csize_t
|
|
# Must copy the data so the `host.msg.input_data` pointer
|
|
# remains valid after the end of `call` lifetime.
|
|
host.input = call.input
|
|
host.msg.input_data = host.input[0].addr
|
|
|
|
let cMsg = hostToComputationMessage(host.msg)
|
|
host.computation = newComputation(vmState, cMsg)
|
|
|
|
return host
|
|
|
|
when defined(evmc_enabled):
|
|
proc doExecEvmc(host: TransactionHost, call: CallParams)
|
|
{.gcsafe, raises: [CatchableError].} =
|
|
var callResult = evmcExecComputation(host)
|
|
let c = host.computation
|
|
|
|
if callResult.status_code == EVMC_SUCCESS:
|
|
c.error = nil
|
|
elif callResult.status_code == EVMC_REVERT:
|
|
c.setError("EVMC_REVERT", false)
|
|
else:
|
|
c.setError($callResult.status_code, true)
|
|
|
|
c.gasMeter.gasRemaining = callResult.gas_left
|
|
c.msg.contractAddress = callResult.create_address.fromEvmc
|
|
c.output = if callResult.output_size <= 0: @[]
|
|
else: @(makeOpenArray(callResult.output_data,
|
|
callResult.output_size.int))
|
|
if not callResult.release.isNil:
|
|
{.gcsafe.}:
|
|
try:
|
|
callResult.release(callResult)
|
|
except Exception as e:
|
|
{.warning: "Kludge(BareExcept): `evmc_release_fn` in vendor package needs to be updated"}
|
|
raiseAssert "Ooops evmcExecComputation(): name=" &
|
|
$e.name & " msg=" & e.msg
|
|
|
|
# FIXME-awkwardFactoring: the factoring out of the pre and
|
|
# post parts feels awkward to me, but for now I'd really like
|
|
# not to have too much duplicated code between sync and async.
|
|
# --Adam
|
|
|
|
proc prepareToRunComputation(host: TransactionHost, call: CallParams) =
|
|
# Must come after `setupHost` for correct fork.
|
|
if not call.noAccessList:
|
|
initialAccessListEIP2929(call)
|
|
|
|
# Charge for gas.
|
|
if not call.noGasCharge:
|
|
host.vmState.mutateStateDB:
|
|
db.subBalance(call.sender, call.gasLimit.u256 * call.gasPrice.u256)
|
|
|
|
proc calculateAndPossiblyRefundGas(host: TransactionHost, call: CallParams): GasInt =
|
|
let c = host.computation
|
|
|
|
# EIP-3529: Reduction in refunds
|
|
let MaxRefundQuotient = if host.vmState.fork >= FkLondon:
|
|
5.GasInt
|
|
else:
|
|
2.GasInt
|
|
|
|
# Calculated gas used, taking into account refund rules.
|
|
if call.noRefund:
|
|
result = c.gasMeter.gasRemaining
|
|
elif not c.shouldBurnGas:
|
|
let maxRefund = (call.gasLimit - c.gasMeter.gasRemaining) div MaxRefundQuotient
|
|
let refund = min(c.getGasRefund(), maxRefund)
|
|
c.gasMeter.returnGas(refund)
|
|
result = c.gasMeter.gasRemaining
|
|
|
|
# Refund for unused gas.
|
|
if result > 0 and not call.noGasCharge:
|
|
host.vmState.mutateStateDB:
|
|
db.addBalance(call.sender, result.u256 * call.gasPrice.u256)
|
|
|
|
proc finishRunningComputation(host: TransactionHost, call: CallParams): CallResult =
|
|
let c = host.computation
|
|
|
|
let gasRemaining = calculateAndPossiblyRefundGas(host, call)
|
|
# evm gas used without intrinsic gas
|
|
host.vmState.tracerGasUsed(host.msg.gas - gasRemaining)
|
|
|
|
result.isError = c.isError
|
|
result.gasUsed = call.gasLimit - gasRemaining
|
|
shallowCopy(result.output, c.output)
|
|
result.contractAddress = if call.isCreate: c.msg.contractAddress
|
|
else: default(HostAddress)
|
|
result.logEntries = host.vmState.stateDB.logEntries()
|
|
result.stack = c.stack
|
|
result.memory = c.memory
|
|
|
|
proc runComputation*(call: CallParams): CallResult
|
|
{.gcsafe, raises: [CatchableError].} =
|
|
let host = setupHost(call)
|
|
prepareToRunComputation(host, call)
|
|
|
|
when defined(evmc_enabled):
|
|
doExecEvmc(host, call)
|
|
else:
|
|
execComputation(host.computation)
|
|
|
|
finishRunningComputation(host, call)
|
|
|
|
# FIXME-duplicatedForAsync
|
|
proc asyncRunComputation*(call: CallParams): Future[CallResult] {.async.} =
|
|
# This has to come before the newComputation call inside setupHost.
|
|
if not call.isCreate:
|
|
await ifNecessaryGetCodeForAccounts(call.vmState, @[call.to.toEvmc.fromEvmc])
|
|
|
|
let host = setupHost(call)
|
|
prepareToRunComputation(host, call)
|
|
|
|
# FIXME-asyncAndEvmc: I'm not sure what to do with EVMC at the moment.
|
|
# when defined(evmc_enabled):
|
|
# doExecEvmc(host, call)
|
|
# else:
|
|
await asyncExecComputation(host.computation)
|
|
|
|
return finishRunningComputation(host, call)
|