nimbus-eth1/nimbus/transaction/call_evm.nim

227 lines
9.3 KiB
Nim

# Nimbus - Various ways of calling the EVM
#
# 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.
import
eth/common/eth_types, stint, options, stew/byteutils,
".."/[vm_types, vm_state, vm_internals, vm_gas_costs, forks],
".."/[db/db_chain, db/accounts_cache, transaction], eth/trie/db,
".."/[config, utils, rpc/hexstrings],
./call_common
type
RpcCallData* = object
source*: EthAddress
to*: EthAddress
gas*: GasInt
gasPrice*: GasInt
value*: UInt256
data*: seq[byte]
contractCreation*: bool
proc rpcRunComputation(vmState: BaseVMState, rpc: RpcCallData,
gasLimit: GasInt, forkOverride = none(Fork),
forEstimateGas: bool = false): CallResult =
return runComputation(CallParams(
vmState: vmState,
forkOverride: forkOverride,
gasPrice: rpc.gasPrice,
gasLimit: gasLimit,
sender: rpc.source,
to: rpc.to,
isCreate: rpc.contractCreation,
value: rpc.value,
input: rpc.data,
# This matches historical behaviour. It might be that not all these steps
# should be disabled for RPC/GraphQL `call`. But until we investigate what
# RPC/GraphQL clients are expecting, keep the same behaviour.
noIntrinsic: not forEstimateGas, # Don't charge intrinsic gas.
noAccessList: not forEstimateGas, # Don't initialise EIP-2929 access list.
noGasCharge: not forEstimateGas, # Don't charge sender account for gas.
noRefund: not forEstimateGas # Don't apply gas refund/burn rule.
))
proc rpcDoCall*(call: RpcCallData, header: BlockHeader, chain: BaseChainDB): HexDataStr =
# TODO: handle revert and error
# TODO: handle contract ABI
# we use current header stateRoot, unlike block validation
# which use previous block stateRoot
# TODO: ^ Check it's correct to use current header stateRoot, not parent
let vmState = newBaseVMState(header.stateRoot, header, chain)
let callResult = rpcRunComputation(vmState, call, call.gas)
return hexDataStr(callResult.output)
proc rpcMakeCall*(call: RpcCallData, header: BlockHeader, chain: BaseChainDB): (string, GasInt, bool) =
# TODO: handle revert
let parent = chain.getBlockHeader(header.parentHash)
let vmState = newBaseVMState(parent.stateRoot, header, chain)
let callResult = rpcRunComputation(vmState, call, call.gas)
return (callResult.output.toHex, callResult.gasUsed, callResult.isError)
func rpcIntrinsicGas(call: RpcCallData, fork: Fork): GasInt =
var intrinsicGas = call.data.intrinsicGas(fork)
if call.contractCreation:
intrinsicGas = intrinsicGas + gasFees[fork][GasTXCreate]
return intrinsicGas
func rpcValidateCall(call: RpcCallData, vmState: BaseVMState, gasLimit: GasInt,
fork: Fork): bool =
# This behaviour matches `validateTransaction`, used by `processTransaction`.
if vmState.cumulativeGasUsed + gasLimit > vmState.blockHeader.gasLimit:
return false
let balance = vmState.readOnlyStateDB.getBalance(call.source)
let gasCost = gasLimit.u256 * call.gasPrice.u256
if gasCost > balance or call.value > balance - gasCost:
return false
let intrinsicGas = rpcIntrinsicGas(call, fork)
if intrinsicGas > gasLimit:
return false
return true
proc rpcEstimateGas*(call: RpcCallData, header: BlockHeader, chain: BaseChainDB, haveGasLimit: bool): GasInt =
# TODO: handle revert and error
var
# we use current header stateRoot, unlike block validation
# which use previous block stateRoot
vmState = newBaseVMState(header.stateRoot, header, chain)
fork = toFork(chain.config, header.blockNumber)
gasLimit = if haveGasLimit: call.gas else: header.gasLimit - vmState.cumulativeGasUsed
# Nimbus `estimateGas` has historically checked against remaining gas in the
# current block, balance in the sender account (even if the sender is default
# account 0x00), and other limits, and returned 0 as the gas estimate if any
# checks failed. This behaviour came from how it used `processTransaction`
# which calls `validateTransaction`. For now, keep this behaviour the same.
# Compare this code with `validateTransaction`.
#
# TODO: This historically differs from `rpcDoCall` and `rpcMakeCall`. There
# are other differences in rpc_utils.nim `callData` too. Are the different
# behaviours intended, and is 0 the correct return value to mean "not enough
# gas to start"? Probably not.
if not rpcValidateCall(call, vmState, gasLimit, fork):
return 0
# Use a db transaction to save and restore the state of the database.
var dbTx = chain.db.beginTransaction()
defer: dbTx.dispose()
let callResult = rpcRunComputation(vmState, call, gasLimit, some(fork), true)
return callResult.gasUsed
proc txCallEvm*(tx: Transaction, sender: EthAddress, vmState: BaseVMState, fork: Fork): GasInt =
var call = CallParams(
vmState: vmState,
forkOverride: some(fork),
gasPrice: tx.gasPrice,
gasLimit: tx.gasLimit,
sender: sender,
to: tx.destination,
isCreate: tx.contractCreation,
value: tx.value,
input: tx.payload
)
if tx.txType > TxLegacy:
shallowCopy(call.accessList, tx.accessList)
return runComputation(call).gasUsed
type
AsmResult* = object
isSuccess*: bool
gasUsed*: GasInt
output*: seq[byte]
stack*: Stack
memory*: Memory
vmState*: BaseVMState
contractAddress*: EthAddress
proc asmCallEvm*(blockNumber: Uint256, chainDB: BaseChainDB, code, data: seq[byte], fork: Fork): AsmResult =
let
parentNumber = blockNumber - 1
parent = chainDB.getBlockHeader(parentNumber)
header = chainDB.getBlockHeader(blockNumber)
headerHash = header.blockHash
body = chainDB.getBlockBody(headerHash)
vmState = newBaseVMState(parent.stateRoot, header, chainDB)
tx = body.transactions[0]
sender = transaction.getSender(tx)
gasLimit = 500000000
gasUsed = 0 #tx.payload.intrinsicGas.GasInt + gasFees[fork][GasTXCreate]
# This is an odd sort of test, where some fields are taken from
# `body.transactions[0]` but other fields (like `gasLimit`) are not. Also it
# creates the new contract using `code` like `CREATE`, but then executes the
# contract like it's `CALL`.
doAssert tx.contractCreation
let contractAddress = generateAddress(sender, vmState.readOnlyStateDB.getNonce(sender))
vmState.mutateStateDB:
db.setCode(contractAddress, code)
let callResult = runComputation(CallParams(
vmState: vmState,
forkOverride: some(fork),
gasPrice: tx.gasPrice,
gasLimit: gasLimit - gasUsed,
sender: sender,
to: contractAddress,
isCreate: false,
value: tx.value,
input: data,
noIntrinsic: true, # Don't charge intrinsic gas.
noAccessList: true, # Don't initialise EIP-2929 access list.
noGasCharge: true, # Don't charge sender account for gas.
noRefund: true # Don't apply gas refund/burn rule.
))
# Some of these are extra returned state, for testing, that a normal EVMC API
# computation doesn't return. We'll have to obtain them outside EVMC.
result.isSuccess = not callResult.isError
result.gasUsed = callResult.gasUsed
result.output = callResult.output
result.stack = callResult.stack
result.memory = callResult.memory
result.vmState = vmState
result.contractAddress = contractAddress
type
FixtureResult* = object
isError*: bool
error*: Error
gasUsed*: GasInt
output*: seq[byte]
vmState*: BaseVMState
logEntries*: seq[Log]
proc fixtureCallEvm*(vmState: BaseVMState, call: RpcCallData,
origin: EthAddress, forkOverride = none(Fork)): FixtureResult =
let callResult = runComputation(CallParams(
vmState: vmState,
forkOverride: forkOverride,
origin: some(origin), # Differs from `rpcSetupComputation`.
gasPrice: call.gasPrice,
gasLimit: call.gas, # Differs from `rpcSetupComputation`.
sender: call.source,
to: call.to,
isCreate: call.contractCreation,
value: call.value,
input: call.data,
noIntrinsic: true, # Don't charge intrinsic gas.
noAccessList: true, # Don't initialise EIP-2929 access list.
noGasCharge: true, # Don't charge sender account for gas.
noRefund: true, # Don't apply gas refund/burn rule.
noTransfer: true, # Don't update balances, nonces, code.
))
# Some of these are extra returned state, for testing, that a normal EVMC API
# computation doesn't return. We'll have to obtain them outside EVMC.
result.isError = callResult.isError
result.error = callResult.error
result.gasUsed = callResult.gasUsed
result.output = callResult.output
result.vmState = vmState
shallowCopy(result.logEntries, callResult.logEntries)