nimbus-eth1/nimbus/transaction/call_evm.nim

208 lines
8.4 KiB
Nim
Raw Normal View History

# 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,
".."/[chain_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)
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
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 =
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
# 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
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
if gasCost > balance or call.value > balance - gasCost:
return false
let intrinsicGas = rpcIntrinsicGas(call, fork)
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
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)
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
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):
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
return 0
# Use a db transaction to save and restore the state of the database.
var dbTx = chain.db.beginTransaction()
defer: dbTx.dispose()
RPC: Change rpcEstimateGas to use the EVM without a fake transaction Change `rpcEstimateGas` to setup and execute a computation directly, in a similar way to `rpcDoCall` and `rpcMakeCall`, instead of constructing a fake transaction and then validating it. This patch does not (or should not) change any behaviour. Although this looks a bit messy as it duplicates parts of `validateTransaction` and `processTransaction`, proc names have been used to hopefully keep the meanings clear, and it's just a stepping stone as those transaction functions will be changed next. Also the behaviour of RPC `estimateGas` may not be correct (although this patch is careful not to change it), so it's good to make it explicit so we can see how it differs from other RPCs. Doing this change exposed some interesting behaviour differences between the `call` RPC and `estimateGas` RPC, which may be bugs, or may be intentional. These differences are now obvious and explicit. The unclear areas are not well documented by any of the clients, even Infura which says a bit more than the others. So to find out if they are intended, we'll have to run tests against other Ethereum services. Guessing, on the face of it, it looks likely that RPC `call` should: - Setup EIP2929 access lists - Account for intrinsic gas (maybe not because zero-gas transactions are ok) And it looks likely that RPC `estimateGas` should: - Not return zero when an account has insufficient balance - Maybe use a different gas cost estimate when one isn't supplied in the RPC Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-03 06:58:45 +00:00
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
proc testCallEvm*(tx: Transaction, sender: EthAddress, vmState: BaseVMState, fork: Fork): CallResult =
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,
noIntrinsic: true, # Don't charge intrinsic gas.
noRefund: true, # Don't apply gas refund/burn rule.
)
if tx.txType > TxLegacy:
shallowCopy(call.accessList, tx.accessList)
runComputation(call)