nimbus-eth1/nimbus/transaction/host_services.nim
Jamie Lokier 6d4205b0b0
Transaction: Just enough support to work with nested calls
Proper nested call functionality is being skipped in this iteration of new EVMC
host code to keep it simpler, to allow testing and architecture to be built
around the less complicated non-nested cases first.

Instead, nested calls use the old `Computation` path, and bypass any
third-party EVM that may be loaded.  The results are the same, and mixing
different EVMs in this way is actually permitted in the EVMC specification.

This approach also means third-party EVMs we test don't need to support
precompiles and we don't need to specially handle those cases.
(E.g. "evmone" doesn't support precompiles, just EVM1 opcodes).

(These before/after scope actions are approximately copy-pasted from
`nimbus/vm/evmc_host.nim`, making their detailed behaviour "obviously correct".
Of course they are subject to tests as well.  The small stack property of
a3c8a5c3 "EVMC: Small stacks when using EVMC, closes #575 (segfaults)" is
carefully retained.)

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:41 +01:00

271 lines
11 KiB
Nim

# Nimbus - Services available to EVM code that is run for a transaction
#
# Copyright (c) 2019-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: [Defect].}
import
sets, times, stint, chronicles,
eth/common/eth_types, ../db/accounts_cache, ../forks,
".."/[vm_types, vm_state, vm_computation, vm_internals],
./host_types, ./host_trace, ./host_call_nested
proc setupTxContext(host: TransactionHost) =
# Conversion issues:
#
# `txContext.tx_gas_price` is 256-bit, but `vmState.txGasPrice` is 64-bit
# signed (`GasInt`), and in reality it tends to be a fairly small integer,
# usually < 16 bits. Our EVM truncates whatever it gets blindly to 64-bit
# anyway. Largest ever so far may be 100,000,000.
# https://medium.com/amberdata/most-expensive-transaction-in-ethereum-blockchain-history-99d9a30d8e02
#
# `txContext.block_number` is 64-bit signed. This is actually too small for
# the Nimbus `BlockNumber` type which is 256-bit (for now), so we truncate
# the other way.
#
# `txContext.chain_id` is 256-bit, but `vmState.chaindb.config.chainId` is
# 64-bit or 32-bit depending on the target CPU architecture (Nim `uint`).
# Our EVM truncates whatever it gets blindly to 64-bit or 32-bit.
#
# No conversion required with the other fields:
#
# `txContext.tx_origin` and `txContext.block_coinbase` are 20-byte Ethereum
# addresses, no issues with these.
#
# `txContext.block_timestamp` is 64-bit signed. `vmState.timestamp.toUnix`
# is from Nim `std/times` and returns `int64` so this matches. (It's
# overkill that we store a full seconds and nanoseconds object in
# `vmState.timestamp` though.)
#
# `txContext.block_gas_limit` is 64-bit signed (EVMC assumes
# [EIP-1985](https://eips.ethereum.org/EIPS/eip-1985) although it's not
# officially accepted), and `vmState.gasLimit` is too (`GasInt`).
#
# `txContext.block_difficulty` is 256-bit, and this one can genuinely take
# values over much of the 256-bit range.
let vmState = host.vmState
host.txContext.tx_gas_price = vmState.txGasPrice.u256.toEvmc
host.txContext.tx_origin = vmState.txOrigin.toEvmc
# vmState.coinbase now unused
host.txContext.block_coinbase = vmState.minerAddress.toEvmc
# vmState.blockNumber now unused
host.txContext.block_number = (vmState.blockHeader.blockNumber
.truncate(typeof(host.txContext.block_number)))
# vmState.timestamp now unused
host.txContext.block_timestamp = vmState.blockHeader.timestamp.toUnix
# vmState.gasLimit now unused
host.txContext.block_gas_limit = vmState.blockHeader.gasLimit
# vmState.difficulty now unused
host.txContext.block_difficulty = vmState.blockHeader.difficulty.toEvmc
host.txContext.chain_id = vmState.chaindb.config.chainId.uint.u256.toEvmc
const use_evmc_glue = defined(evmc_enabled)
# When using the EVMC binary interface, each of the functions below is wrapped
# in another function that converts types to be compatible with the binary
# interface, and the functions below are not called directly. The conversions
# mostly just cast between byte-compatible types, so to avoid a redundant call
# layer, make the functions below `{.inline.}` when wrapped in this way.
when use_evmc_glue:
{.push inline.}
proc accountExists(host: TransactionHost, address: HostAddress): bool {.show.} =
if host.vmState.fork >= FkSpurious:
not host.vmState.readOnlyStateDB.isDeadAccount(address)
else:
host.vmState.readOnlyStateDB.accountExists(address)
# TODO: Why is `address` an argument in `getStorage`, `setStorage` and
# `selfDestruct`, if an EVM is only allowed to do these things to its own
# contract account and the host always knows which account?
proc getStorage(host: TransactionHost, address: HostAddress, key: HostKey): HostValue {.show.} =
host.vmState.readOnlyStateDB.getStorage(address, key)
const
# EIP-1283
SLOAD_GAS_CONSTANTINOPLE = 200
# EIP-2200
SSTORE_SET_GAS = 20000
SSTORE_RESET_GAS = 5000
SSTORE_CLEARS_SCHEDULE = 15000
SLOAD_GAS_ISTANBUL = 800
# EIP-2929
WARM_STORAGE_READ_COST = 100
COLD_SLOAD_COST = 2100
COLD_ACCOUNT_ACCESS_COST = 2600
func storageModifiedAgainRefund(originalValue, oldValue, value: HostValue,
fork: Fork): int {.inline.} =
# Calculate `SSTORE` refund according to EIP-2929 (Berlin),
# EIP-2200 (Istanbul) or EIP-1283 (Constantinople only).
result = 0
if value == originalValue:
result = if value.isZero: SSTORE_SET_GAS
elif fork >= FkBerlin: SSTORE_RESET_GAS - COLD_SLOAD_COST
else: SSTORE_RESET_GAS
let SLOAD_GAS = if fork >= FkBerlin: WARM_STORAGE_READ_COST
elif fork >= FkIstanbul: SLOAD_GAS_ISTANBUL
else: SLOAD_GAS_CONSTANTINOPLE
result -= SLOAD_GAS
if not originalValue.isZero:
if value.isZero:
result += SSTORE_CLEARS_SCHEDULE
elif oldValue.isZero:
result -= SSTORE_CLEARS_SCHEDULE
proc setStorage(host: TransactionHost, address: HostAddress,
key: HostKey, value: HostValue): EvmcStorageStatus {.show.} =
let db = host.vmState.readOnlyStateDB
let oldValue = db.getStorage(address, key)
if oldValue == value:
return EVMC_STORAGE_UNCHANGED
host.vmState.mutateStateDB:
db.setStorage(address, key, value)
if host.vmState.fork >= FkIstanbul or host.vmState.fork == FkConstantinople:
let originalValue = db.getCommittedStorage(address, key)
if oldValue != originalValue:
# Gas refund for `MODIFIED_AGAIN` (EIP-1283/2200/2929 only).
let refund = storageModifiedAgainRefund(originalValue, oldValue, value,
host.vmState.fork)
# TODO: Refund depends on `Computation` at the moment.
if refund != 0:
host.computation.gasMeter.refundGas(refund)
return EVMC_STORAGE_MODIFIED_AGAIN
if oldValue.isZero:
return EVMC_STORAGE_ADDED
elif value.isZero:
# Gas refund for `DELETED` (all forks).
# TODO: Refund depends on `Computation` at the moment.
host.computation.gasMeter.refundGas(SSTORE_CLEARS_SCHEDULE)
return EVMC_STORAGE_DELETED
else:
return EVMC_STORAGE_MODIFIED
proc getBalance(host: TransactionHost, address: HostAddress): HostBalance {.show.} =
host.vmState.readOnlyStateDB.getBalance(address)
proc getCodeSize(host: TransactionHost, address: HostAddress): HostSize {.show.} =
# TODO: Check this `HostSize`, it was copied as `uint` from other code.
# Note: Old `evmc_host` uses `getCode(address).len` instead.
host.vmState.readOnlyStateDB.getCodeSize(address).HostSize
proc getCodeHash(host: TransactionHost, address: HostAddress): HostHash {.show.} =
let db = host.vmState.readOnlyStateDB
# TODO: Copied from `Computation`, but check if that code is wrong with
# `FkSpurious`, as it has different calls from `accountExists` above.
if not db.accountExists(address) or db.isEmptyAccount(address):
default(HostHash)
else:
db.getCodeHash(address)
proc copyCode(host: TransactionHost, address: HostAddress,
code_offset: HostSize, buffer_data: ptr byte,
buffer_size: HostSize): HostSize {.show.} =
# We must handle edge cases carefully to prevent overflows. `len` is signed
# type `int`, but `code_offset` and `buffer_size` are _unsigned_, and may
# have large values (deliberately if attacked) that exceed the range of `int`.
#
# Comparing signed and unsigned types is _unsafe_: A type-conversion will
# take place which breaks the comparison for some values. So here we use
# explicit type-conversions, always compare the same types, and always
# convert towards the type that cannot truncate because preceding checks have
# been used to reduce the possible value range.
#
# Note, when there is no code, `getCode` result is empty `seq`. It was `nil`
# when the DB was first implemented, due to Nim language changes since then.
var code: seq[byte] = host.vmState.readOnlyStateDB.getCode(address)
var safe_len: int = code.len # It's safe to assume >= 0.
if code_offset >= safe_len.HostSize:
return 0
let safe_offset = code_offset.int
safe_len = safe_len - safe_offset
if buffer_size < safe_len.HostSize:
safe_len = buffer_size.int
if safe_len > 0:
copyMem(buffer_data, code[safe_offset].addr, safe_len)
return safe_len.HostSize
proc selfDestruct(host: TransactionHost, address, beneficiary: HostAddress) {.show.} =
host.vmState.mutateStateDB:
let closingBalance = db.getBalance(address)
let beneficiaryBalance = db.getBalance(beneficiary)
# Transfer to beneficiary
db.setBalance(beneficiary, beneficiaryBalance + closingBalance)
# Zero balance of account being deleted.
# This must come after sending to the beneficiary in case the
# contract named itself as the beneficiary.
db.setBalance(address, 0.u256)
# TODO: Calling via `computation` is necessary to make some tests pass.
# Here's one that passes only with this:
# tests/fixtures/eth_tests/GeneralStateTests/stRandom2/randomStatetest487.json
# We can't keep using `computation` though.
host.computation.touchedAccounts.incl(beneficiary)
host.computation.selfDestructs.incl(address)
#host.touchedAccounts.incl(beneficiary)
#host.selfDestructs.incl(address)
proc call(host: TransactionHost, msg: EvmcMessage): EvmcResult {.show.} =
return host.callEvmcNested(msg)
proc getTxContext(host: TransactionHost): EvmcTxContext {.show.} =
if not host.cachedTxContext:
host.setupTxContext()
host.cachedTxContext = true
return host.txContext
proc getBlockHash(host: TransactionHost, number: HostBlockNumber): HostHash {.show.} =
# TODO: Clean up the different messy block number types.
host.vmState.getAncestorHash(number.toBlockNumber)
proc emitLog(host: TransactionHost, address: HostAddress,
data: ptr byte, data_size: HostSize,
topics: ptr HostTopic, topics_count: HostSize) {.show.} =
var log: Log
# Note, this assumes the EVM ensures `data_size` and `topics_count` cannot be
# unreasonably large values. Largest `topics_count` should be 4 according to
# EVMC documentation, but we won't restrict it here.
if topics_count > 0:
let topicsArray = cast[ptr UncheckedArray[HostTopic]](topics)
let count = topics_count.int
log.topics = newSeq[Topic](count)
for i in 0 ..< count:
log.topics[i] = topicsArray[i]
if (data_size > 0):
log.data = newSeq[byte](data_size.int)
copyMem(log.data[0].addr, data, data_size.int)
log.address = address
# TODO: Calling via `computation` is necessary to makes some tests pass.
# Here's one that passes only with this:
# tests/fixtures/eth_tests/GeneralStateTests/stRandom2/randomStatetest583.json
# We can't keep using `computation` though.
host.computation.logEntries.add(log)
#host.logEntries.add(log)
when use_evmc_glue:
{.pop: inline.}
const included_from_host_services = true
include ./evmc_host_glue
else:
export
accountExists, getStorage, storage, getBalance, getCodeSize, getCodeHash,
copyCode, selfDestruct, getTxContext, call, getBlockHash, emitLog