mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-16 23:31:16 +00:00
b3cb51e89e
The EVM stack is a hot spot in EVM execution and we end up paying a nim seq tax in several ways, adding up to ~5% of execution time: * on initial allocation, all bytes get zeroed - this means we have to choose between allocating a full stack or just a partial one and then growing it * pushing and popping introduce additional zeroing * reallocations on growth copy + zero - expensive again! * redundant range checking on every operation reducing inlining etc Here a custom stack using C memory is instroduced: * no zeroing on allocation * full stack allocated on EVM startup -> no reallocation during execution * fast push/pop - no zeroing again * 32-byte alignment - this makes it easier for the compiler to use vector instructions * no stack allocated for precompiles (these never use it anyway) Of course, this change also means we have to manage memory manually - for the EVM, this turns out to be not too bad because we already manage database transactions the same way (they have to be freed "manually") so we can simply latch on to this mechanism. While we're at it, this PR also skips database lookup for known precompiles by resolving such addresses earlier.
337 lines
11 KiB
Nim
337 lines
11 KiB
Nim
# Nimbus - Common entry point to the EVM from all different callers
|
|
#
|
|
# Copyright (c) 2018-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, stint, stew/ptrops,
|
|
chronos,
|
|
results,
|
|
stew/saturation_arith,
|
|
../evm/[types, state],
|
|
../evm/[precompiles, internals],
|
|
../db/ledger,
|
|
../common/evmforks,
|
|
../core/eip4844,
|
|
../core/eip7702,
|
|
./host_types,
|
|
./call_types
|
|
|
|
import ../evm/computation except fromEvmc, toEvmc
|
|
|
|
when defined(evmc_enabled):
|
|
import
|
|
../utils/utils,
|
|
./host_services
|
|
else:
|
|
import
|
|
../evm/state_transactions
|
|
|
|
export
|
|
call_types
|
|
|
|
proc hostToComputationMessage*(msg: EvmcMessage): Message =
|
|
Message(
|
|
kind: CallKind(msg.kind.ord),
|
|
depth: msg.depth,
|
|
gas: GasInt 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: msg.flags
|
|
)
|
|
|
|
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)
|
|
# If the `call.to` has a delegation, also warm its target.
|
|
let target = parseDelegationAddress(db.getCode(call.to))
|
|
if target.isSome:
|
|
db.accessList(target[])
|
|
|
|
# EIP3651 adds coinbase to the list of addresses that should start warm.
|
|
if vmState.fork >= FkShanghai:
|
|
db.accessList(vmState.coinbase)
|
|
|
|
# Adds the correct subset of precompiles.
|
|
for c in activePrecompiles(vmState.fork):
|
|
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, key.to(UInt256))
|
|
|
|
proc preExecComputation(vmState: BaseVMState, call: CallParams): int64 =
|
|
var gasRefund = 0
|
|
let ledger = vmState.stateDB
|
|
|
|
if not call.isCreate:
|
|
ledger.incNonce(call.sender)
|
|
|
|
# EIP-7702
|
|
for auth in call.authorizationList:
|
|
# 1. Verify the chain id is either 0 or the chain's current ID.
|
|
if not(auth.chainId == 0.ChainId or auth.chainId == vmState.com.chainId):
|
|
continue
|
|
|
|
# 2. authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]
|
|
let authority = authority(auth).valueOr:
|
|
continue
|
|
|
|
# 3. Add authority to accessed_addresses (as defined in EIP-2929.)
|
|
ledger.accessList(authority)
|
|
|
|
# 4. Verify the code of authority is either empty or already delegated.
|
|
let code = ledger.getCode(authority)
|
|
if code.len > 0:
|
|
if not parseDelegation(code):
|
|
continue
|
|
|
|
# 5. Verify the nonce of authority is equal to nonce.
|
|
if ledger.getNonce(authority) != auth.nonce:
|
|
continue
|
|
|
|
# 6. Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter if authority exists in the trie.
|
|
if ledger.accountExists(authority):
|
|
gasRefund += PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST
|
|
|
|
# 7. Set the code of authority to be 0xef0100 || address. This is a delegation designation.
|
|
if auth.address == default(eth_types.Address):
|
|
ledger.setCode(authority, @[])
|
|
else:
|
|
ledger.setCode(authority, @(addressToDelegation(auth.address)))
|
|
|
|
# 8. Increase the nonce of authority by one.
|
|
ledger.setNonce(authority, auth.nonce + 1)
|
|
|
|
# Usually the transaction destination and delegation target are added to
|
|
# the access list in initialAccessListEIP2929, however if the delegation is in
|
|
# the same transaction we need add here as to reduce calling slow ecrecover.
|
|
if call.to == authority:
|
|
ledger.accessList(auth.address)
|
|
|
|
gasRefund
|
|
|
|
proc setupHost(call: CallParams, keepStack: bool): TransactionHost =
|
|
let vmState = call.vmState
|
|
vmState.txCtx = TxContext(
|
|
origin : call.origin.get(call.sender),
|
|
gasPrice : call.gasPrice,
|
|
versionedHashes: call.versionedHashes,
|
|
blobBaseFee : getBlobBaseFee(vmState.blockCtx.excessBlobGas),
|
|
)
|
|
|
|
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: int64.saturate(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.
|
|
)
|
|
|
|
let gasRefund = if call.sysCall: 0
|
|
else: preExecComputation(vmState, call)
|
|
let isPrecompile =
|
|
not call.isCreate and vmState.fork.getPrecompile(host.msg.code_address.fromEvmc).isSome()
|
|
|
|
# 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: CodeBytesRef
|
|
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 = CodeBytesRef.init(call.input)
|
|
else:
|
|
# TODO: Share the underlying data, but only after checking this does not
|
|
# cause problems with the database.
|
|
if isPrecompile:
|
|
code = nil
|
|
elif host.vmState.fork >= FkPrague:
|
|
code = host.vmState.readOnlyStateDB.resolveCode(host.msg.code_address.fromEvmc)
|
|
else:
|
|
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, call.sysCall, cMsg, code, isPrecompile = isPrecompile, keepStack = keepStack)
|
|
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, call.sysCall, cMsg, isPrecompile = isPrecompile, keepStack = keepStack)
|
|
|
|
host.computation.gasMeter.refundGas(gasRefund)
|
|
vmState.captureStart(host.computation, call.sender, call.to,
|
|
call.isCreate, call.input,
|
|
call.gasLimit, call.value)
|
|
|
|
return host
|
|
|
|
when defined(evmc_enabled):
|
|
proc doExecEvmc(host: TransactionHost, call: CallParams) =
|
|
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 = GasInt 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.}:
|
|
callResult.release(callResult)
|
|
|
|
# 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:
|
|
let
|
|
vmState = host.vmState
|
|
fork = vmState.fork
|
|
|
|
vmState.mutateStateDB:
|
|
db.subBalance(call.sender, call.gasLimit.u256 * call.gasPrice.u256)
|
|
|
|
# EIP-4844
|
|
if fork >= FkCancun:
|
|
let blobFee = calcDataFee(call.versionedHashes.len,
|
|
vmState.blockCtx.excessBlobGas)
|
|
db.subBalance(call.sender, blobFee)
|
|
|
|
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, T: type): T =
|
|
let c = host.computation
|
|
|
|
let gasRemaining = calculateAndPossiblyRefundGas(host, call)
|
|
# evm gas used without intrinsic gas
|
|
let gasUsed = host.msg.gas.GasInt - gasRemaining
|
|
host.vmState.captureEnd(c, c.output, gasUsed, c.errorOpt)
|
|
|
|
when T is CallResult|DebugCallResult:
|
|
# Collecting the result can be unnecessarily expensive when (re)-processing
|
|
# transactions
|
|
if c.isError:
|
|
result.error = c.error.info
|
|
result.gasUsed = call.gasLimit - gasRemaining
|
|
result.output = system.move(c.output)
|
|
result.contractAddress = if call.isCreate: c.msg.contractAddress
|
|
else: default(HostAddress)
|
|
|
|
when T is DebugCallResult:
|
|
result.stack = move(c.finalStack)
|
|
result.memory = move(c.memory)
|
|
elif T is GasInt:
|
|
result = call.gasLimit - gasRemaining
|
|
elif T is string:
|
|
if c.isError:
|
|
result = c.error.info
|
|
elif T is seq[byte]:
|
|
result = move(c.output)
|
|
else:
|
|
{.error: "Unknown computation output".}
|
|
|
|
proc runComputation*(call: CallParams, T: type): T =
|
|
let host = setupHost(call, keepStack = T is DebugCallResult)
|
|
prepareToRunComputation(host, call)
|
|
|
|
when defined(evmc_enabled):
|
|
doExecEvmc(host, call)
|
|
else:
|
|
if host.computation.sysCall:
|
|
execSysCall(host.computation)
|
|
else:
|
|
execComputation(host.computation)
|
|
|
|
finishRunningComputation(host, call, T)
|