Speed up evm stack (#2881)

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.
This commit is contained in:
Jacek Sieka 2024-11-30 10:07:10 +01:00 committed by GitHub
parent b2a4373cc9
commit b3cb51e89e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 270 additions and 200 deletions

View File

@ -453,7 +453,6 @@ proc getNonce*(ac: LedgerRef, address: Address): AccountNonce =
proc getCode*(ac: LedgerRef,
address: Address,
returnHash: static[bool] = false): auto =
# Always returns non-nil!
let acc = ac.getAccount(address, false)
if acc.isNil:
when returnHash:
@ -526,7 +525,7 @@ proc getDelegateAddress*(ac: LedgerRef, address: Address): Address =
let delegateTo = parseDelegationAddress(code).valueOr:
return
delegateTo
proc getCommittedStorage*(ac: LedgerRef, address: Address, slot: UInt256): UInt256 =
let acc = ac.getAccount(address, false)
if acc.isNil:

View File

@ -11,6 +11,7 @@
{.push raises: [].}
import
std/sequtils,
".."/[db/ledger, constants],
"."/[code_stream, memory, message, stack, state],
"."/[types],
@ -21,8 +22,7 @@ import
../utils/utils,
../common/common,
eth/common/eth_types_rlp,
chronicles, chronos,
sets
chronicles, chronos
export
common
@ -260,39 +260,44 @@ template resolveCode*(c: Computation, address: Address): CodeBytesRef =
c.vmState.readOnlyStateDB.resolveCode(address)
proc newComputation*(vmState: BaseVMState, sysCall: bool, message: Message,
salt: ContractSalt = ZERO_CONTRACTSALT): Computation =
isPrecompile, keepStack: bool, salt: ContractSalt = ZERO_CONTRACTSALT): Computation =
new result
result.vmState = vmState
result.msg = message
result.memory = EvmMemory.init()
result.stack = EvmStack.init()
result.returnStack = @[]
result.gasMeter.init(message.gas)
result.sysCall = sysCall
result.keepStack = keepStack
if result.msg.isCreate():
result.msg.contractAddress = result.generateContractAddress(salt)
result.code = CodeStream.init(message.data)
message.data = @[]
else:
if vmState.fork >= FkPrague:
result.code = CodeStream.init(
vmState.readOnlyStateDB.resolveCode(message.codeAddress))
if not isPrecompile:
result.memory = EvmMemory.init()
result.stack = EvmStack.init()
if result.msg.isCreate():
result.msg.contractAddress = result.generateContractAddress(salt)
result.code = CodeStream.init(message.data)
message.data = @[]
else:
result.code = CodeStream.init(
vmState.readOnlyStateDB.getCode(message.codeAddress))
if vmState.fork >= FkPrague:
result.code = CodeStream.init(
vmState.readOnlyStateDB.resolveCode(message.codeAddress))
else:
result.code = CodeStream.init(
vmState.readOnlyStateDB.getCode(message.codeAddress))
func newComputation*(vmState: BaseVMState, sysCall: bool,
message: Message, code: CodeBytesRef): Computation =
message: Message, code: CodeBytesRef, isPrecompile, keepStack: bool, ): Computation =
new result
result.vmState = vmState
result.msg = message
result.memory = EvmMemory.init()
result.stack = EvmStack.init()
result.returnStack = @[]
result.gasMeter.init(message.gas)
result.code = CodeStream.init(code)
result.sysCall = sysCall
result.keepStack = keepStack
if not isPrecompile:
result.code = CodeStream.init(code)
result.memory = EvmMemory.init()
result.stack = EvmStack.init()
template gasCosts*(c: Computation): untyped =
c.vmState.gasCosts
@ -317,6 +322,12 @@ proc commit*(c: Computation) =
proc dispose*(c: Computation) =
c.vmState.stateDB.safeDispose(c.savePoint)
if c.stack != nil:
if c.keepStack:
c.finalStack = toSeq(c.stack.items())
c.stack.dispose()
c.stack = nil
c.savePoint = nil
proc rollback*(c: Computation) =

View File

@ -34,7 +34,7 @@ proc blockhashOp(cpt: VmCpt): EvmResultVoid =
## 0x40, Get the hash of one of the 256 most recent complete blocks.
template block256(top, number, conv) =
if number > high(BlockNumber).u256:
top = zero(UInt256)
conv(zero(UInt256), top)
else:
conv(cpt.getBlockHash(number.truncate(BlockNumber)), top)
@ -82,7 +82,7 @@ proc blobHashOp(cpt: VmCpt): EvmResultVoid =
if index < len:
conv(cpt.getVersionedHash(index).data, top)
else:
top = zero(UInt256)
conv(zero(UInt256), top)
cpt.stack.unaryWithTop(blob256)

View File

@ -21,6 +21,7 @@ import
../../../core/eip7702,
../../computation,
../../memory,
../../precompiles,
../../stack,
../../types,
../gas_costs,
@ -215,7 +216,9 @@ else:
# need to provide explicit <c> and <child> for capturing in chainTo proc()
# <memPos> and <memLen> are provided by value and need not be captured
var
child = newComputation(c.vmState, false, childMsg)
precompile = getPrecompile(c.fork, childMsg.codeAddress)
child = newComputation(
c.vmState, false, childMsg, isPrecompile = precompile.isSome(), keepStack = false)
c.chainTo(child):
if not child.shouldBurnGas:

View File

@ -67,7 +67,7 @@ else:
# need to provide explicit <c> and <child> for capturing in chainTo proc()
var
child = newComputation(c.vmState, false, childMsg, salt)
child = newComputation(c.vmState, false, childMsg, false, false, salt)
c.chainTo(child):
if not child.shouldBurnGas:

View File

@ -68,7 +68,7 @@ proc logImpl(c: Computation, opcode: Op, topicCount: static int): EvmResultVoid
when evmc_enabled:
var topics: array[4, evmc_bytes32]
for i in 0 ..< topicCount:
topics[i].bytes = c.stack.lsPeekTopic(^(i+3))
topics[i].bytes = c.stack.lsPeekTopic(^(i+3)).data
c.host.emitLog(c.msg.contractAddress,
c.memory.read(memPos, len),
@ -77,7 +77,7 @@ proc logImpl(c: Computation, opcode: Op, topicCount: static int): EvmResultVoid
var log: Log
log.topics = newSeqOfCap[Topic](topicCount)
for i in 0 ..< topicCount:
log.topics.add Bytes32 c.stack.lsPeekTopic(^(i+3))
log.topics.add c.stack.lsPeekTopic(^(i+3))
assign(log.data, c.memory.read(memPos, len))
log.address = c.msg.contractAddress

View File

@ -178,7 +178,7 @@ proc mstore8Op(cpt: VmCpt): EvmResultVoid =
proc sloadOp(cpt: VmCpt): EvmResultVoid =
## 0x54, Load word from storage.
template sload256(top, slot, conv) =
top = cpt.getStorage(slot)
conv(cpt.getStorage(slot), top)
cpt.stack.unaryWithTop(sload256)
proc sloadEIP2929Op(cpt: VmCpt): EvmResultVoid =
@ -186,7 +186,7 @@ proc sloadEIP2929Op(cpt: VmCpt): EvmResultVoid =
template sloadEIP2929(top, slot, conv) =
let gasCost = cpt.gasEip2929AccountCheck(cpt.msg.contractAddress, slot)
? cpt.opcodeGasCost(Sload, gasCost, reason = "sloadEIP2929")
top = cpt.getStorage(slot)
conv(cpt.getStorage(slot), top)
cpt.stack.unaryWithTop(sloadEIP2929)
# -------

View File

@ -197,8 +197,11 @@ proc executeOpcodes*(c: Computation, shouldPrepareTracer: bool = true) =
let fork = c.fork
block blockOne:
if c.continuation.isNil and c.execPrecompiles(fork):
break blockOne
if c.continuation.isNil:
let precompile = c.fork.getPrecompile(c.msg.codeAddress)
if precompile.isSome:
c.execPrecompile(precompile[])
break blockOne
let cont = c.continuation
if not cont.isNil:

View File

@ -70,10 +70,6 @@ func getMaxPrecompileAddr(fork: EVMFork): PrecompileAddresses =
func validPrecompileAddr(addrByte, maxPrecompileAddr: byte): bool =
(addrByte in PrecompileAddresses.low.byte .. maxPrecompileAddr)
func validPrecompileAddr(addrByte: byte, fork: EVMFork): bool =
let maxPrecompileAddr = getMaxPrecompileAddr(fork)
validPrecompileAddr(addrByte, maxPrecompileAddr.byte)
func getSignature(c: Computation): EvmResult[SigRes] =
# input is Hash, V, R, S
template data: untyped = c.msg.data
@ -719,16 +715,21 @@ func activePrecompilesList*(fork: EVMFork): seq[Address] =
for address in activePrecompiles(fork):
result.add address
proc execPrecompiles*(c: Computation, fork: EVMFork): bool =
proc getPrecompile*(fork: EVMFork, b: byte): Opt[PrecompileAddresses] =
let maxPrecompileAddr = getMaxPrecompileAddr(fork)
if validPrecompileAddr(b, maxPrecompileAddr.byte):
Opt.some(PrecompileAddresses(b))
else:
Opt.none(PrecompileAddresses)
proc getPrecompile*(fork: EVMFork, codeAddress: Address): Opt[PrecompileAddresses] =
for i in 0..18:
if c.msg.codeAddress.data[i] != 0:
return false
if codeAddress.data[i] != 0:
return Opt.none(PrecompileAddresses)
getPrecompile(fork, codeAddress.data[19])
let lb = c.msg.codeAddress.data[19]
if not validPrecompileAddr(lb, fork):
return false
let precompile = PrecompileAddresses(lb)
proc execPrecompile*(c: Computation, precompile: PrecompileAddresses) =
let fork = c.fork
let res = case precompile
of paEcRecover: ecRecover(c)
of paSha256: sha256(c)
@ -759,5 +760,3 @@ proc execPrecompiles*(c: Computation, fork: EVMFork): bool =
else:
# swallow any other precompiles errors
debug "execPrecompiles validation error", errCode = $res.error.code
true

View File

@ -8,182 +8,200 @@
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
# Type managing the EVM stack that comprises of 1024 256-bit words.
#
# The stack is a hot spot in EVM execution since it's used for practically every
# opcode. We use custom-allocated memory for several reasons, chiefly
# performance (at the time of writing, using a seq carried about 5% overhead on
# total EVM execution time):
#
# * no zeromem - the way the EVM uses the stack, it always writes full words
# meaning that whatever zeroing was done gets overwritten anyway - compilers
# are typically not smart enough to get rid of all of this
# * no reallocation - since we can allocate memory without zeroing, we can
# allocate the full stack length on creation and never grow / reallocate
# * less redundant range checking - we have to perform range checks manually and
# the compiler is not able to remove them consistently even though we range
# check manually
# * 32-byte alignment helps vector instruction optimization
#
# After calling `init`, the stack must be freed manually using `dispose`!
{.push raises: [].}
import
system/ansi_c,
stew/[assign2, ptrops],
stint,
eth/common/[base, addresses, hashes],
std/[macros],
std/typetraits,
./evm_errors,
./interpreter/utils/utils_numeric
const evmStackSize = 1024
## https://ethereum.org/en/developers/docs/evm/#evm-instructions
type
EvmStack* = ref object
values: seq[EvmStackElement]
values: ptr EvmStackElement
memory: pointer
len*: int
EvmStackElement = object
data {.align: 32.}: UInt256
EvmStackElement = UInt256
EvmStackInts = uint64 | uint | int | GasInt
EvmStackBytes32 = array[32, byte]
func len*(stack: EvmStack): int {.inline.} =
len(stack.values)
static:
# A few sanity checks because we skip the GC / parts of the nim type system:
doAssert sizeof(UInt256) == 32, "no padding etc"
doAssert supportsCopyMem(EvmStackElement), "byte-based ops must work sanely"
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
template toStackElem(v: UInt256, elem: EvmStackElement) =
template `[]`*(s: EvmStack, i: int): EvmStackElement =
s.values.offset(i)[]
template `[]`*(s: EvmStack, i: BackwardsIndex): EvmStackElement =
s.values.offset(s.len - int(i))[]
template `[]=`*(s: EvmStack, i: int, v: EvmStackElement) =
assign(s[i], v)
template `[]=`*(s: EvmStack, i: BackwardsIndex, v: EvmStackElement) =
assign(s[i], v)
template toStackElem(v: EvmStackElement, elem: EvmStackElement) =
elem = v
template toStackElem(v: UInt256, elem: EvmStackElement) =
elem.data = v
template toStackElem(v: EvmStackInts, elem: EvmStackElement) =
elem = v.u256
elem.data = v.u256
template toStackElem(v: Address, elem: EvmStackElement) =
elem.initFromBytesBE(v.data)
elem.data.initFromBytesBE(v.data)
template toStackElem(v: Hash32, elem: EvmStackElement) =
elem.initFromBytesBE(v.data)
elem.data.initFromBytesBE(v.data)
template toStackElem(v: openArray[byte], elem: EvmStackElement) =
doAssert(v.len <= 32)
elem.initFromBytesBE(v)
elem.data.initFromBytesBE(v)
template fromStackElem(elem: EvmStackElement, _: type UInt256): UInt256 =
elem
elem.data
func fromStackElem(elem: EvmStackElement, _: type Address): Address =
elem.to(Bytes32).to(Address)
elem.data.to(Bytes32).to(Address)
template fromStackElem(elem: EvmStackElement, _: type Hash32): Hash32 =
Hash32(elem.toBytesBE())
Hash32(elem.data.toBytesBE())
template fromStackElem(elem: EvmStackElement, _: type EvmStackBytes32): EvmStackBytes32 =
elem.toBytesBE()
func pushAux[T](stack: var EvmStack, value: T): EvmResultVoid =
if len(stack.values) > 1023:
return err(stackErr(StackFull))
stack.values.setLen(stack.values.len + 1)
toStackElem(value, stack.values[^1])
ok()
template fromStackElem(elem: EvmStackElement, _: type Bytes32): Bytes32 =
elem.data.toBytesBE().to(Bytes32)
func ensurePop(stack: EvmStack, expected: int): EvmResultVoid =
if stack.values.len < expected:
if stack.len < expected:
return err(stackErr(StackInsufficient))
ok()
func popAux(stack: var EvmStack, T: type): EvmResult[T] =
func popAux(stack: EvmStack, T: type): EvmResult[T] =
? ensurePop(stack, 1)
result = ok(fromStackElem(stack.values[^1], T))
stack.values.setLen(stack.values.len - 1)
func internalPopTuple(stack: var EvmStack, T: type, tupleLen: static[int]): EvmResult[T] =
? ensurePop(stack, tupleLen)
var
i = 0
v: T
let sz = stack.values.high
for f in fields(v):
f = fromStackElem(stack.values[sz - i], UInt256)
inc i
stack.values.setLen(sz - tupleLen + 1)
ok(v)
macro genTupleType(len: static[int], elemType: untyped): untyped =
result = nnkTupleConstr.newNimNode()
for i in 0 ..< len: result.add(elemType)
stack.len -= 1
ok(fromStackElem(stack[stack.len], T))
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
func push*(stack: var EvmStack,
value: EvmStackInts | UInt256 | Address | Hash32): EvmResultVoid =
pushAux(stack, value)
func push*(stack: EvmStack,
value: EvmStackElement | EvmStackInts | UInt256 | Address | Hash32): EvmResultVoid =
let len = stack.len
if len > 1023:
return err(stackErr(StackFull))
toStackElem(value, stack[len])
stack.len = len + 1
ok()
func push*(stack: var EvmStack, value: openArray[byte]): EvmResultVoid =
pushAux(stack, value)
func popInt*(stack: var EvmStack): EvmResult[UInt256] =
func popInt*(stack: EvmStack): EvmResult[UInt256] =
popAux(stack, UInt256)
func popSafeInt*(stack: var EvmStack): EvmResult[int] =
? ensurePop(stack, 1)
result = ok(fromStackElem(stack.values[^1], UInt256).safeInt)
stack.values.setLen(stack.values.len - 1)
func popMemRef*(stack: var EvmStack): EvmResult[int] =
? ensurePop(stack, 1)
result = ok(fromStackElem(stack.values[^1], UInt256).cleanMemRef)
stack.values.setLen(stack.values.len - 1)
func popInt*(stack: var EvmStack, numItems: static[int]): auto =
type T = genTupleType(numItems, UInt256)
stack.internalPopTuple(T, numItems)
func popAddress*(stack: var EvmStack): EvmResult[Address] =
func popAddress*(stack: EvmStack): EvmResult[Address] =
popAux(stack, Address)
func popTopic*(stack: var EvmStack): EvmResult[EvmStackBytes32] =
popAux(stack, EvmStackBytes32)
proc init*(_: type EvmStack): EvmStack =
let memory = c_malloc(evmStackSize * sizeof(EvmStackElement) + 31)
func init*(_: type EvmStack): EvmStack =
EvmStack(
values: newSeqOfCap[EvmStackElement](128)
values: cast[ptr EvmStackElement](((cast[uint](memory) + 31) div 32) * 32) ,
memory: memory, # Need to free the same pointer that we got from malloc
len: 0,
)
func swap*(stack: var EvmStack, position: int): EvmResultVoid =
## Perform a SWAP operation on the stack
let idx = position + 1
if idx < stack.values.len + 1:
(stack.values[^1], stack.values[^idx]) = (stack.values[^idx], stack.values[^1])
proc dispose*(stack: EvmStack) =
if stack[].memory != nil:
c_free(stack[].memory)
stack[].reset()
func swap*(stack: EvmStack, position: static int): EvmResultVoid =
## Swap the `top` and `top - position` items
let
idx = position + 1 # locals help compiler reason about overflows
len = stack.len
if stack.len >= idx:
let
l1 = len - 1
li = len - idx
let tmp {.noinit.} = stack[l1]
stack[l1] = stack[li]
stack[li] = tmp
ok()
else:
err(stackErr(StackInsufficient))
func dup*(stack: var EvmStack, position: int): EvmResultVoid =
## Perform a DUP operation on the stack
func dup*(stack: EvmStack, position: int): EvmResultVoid =
## Push copy of item at `top - position`
if position in 1 .. stack.len:
stack.push(stack.values[^position])
stack.push(stack[^position])
else:
err(stackErr(StackInsufficient))
func peek*(stack: EvmStack): EvmResult[UInt256] =
if stack.values.len == 0:
return err(stackErr(StackInsufficient))
ok(fromStackElem(stack.values[^1], UInt256))
? ensurePop(stack, 1)
ok(fromStackElem(stack[^1], UInt256))
func peekSafeInt*(stack: EvmStack): EvmResult[int] =
if stack.values.len == 0:
return err(stackErr(StackInsufficient))
ok(fromStackElem(stack.values[^1], UInt256).safeInt)
? ensurePop(stack, 1)
ok(fromStackElem(stack[^1], UInt256).safeInt)
func `[]`*(stack: EvmStack, i: BackwardsIndex, T: typedesc): EvmResult[T] =
? ensurePop(stack, int(i))
ok(fromStackElem(stack.values[i], T))
ok(fromStackElem(stack[i], T))
func peekInt*(stack: EvmStack): EvmResult[UInt256] =
? ensurePop(stack, 1)
ok(fromStackElem(stack.values[^1], UInt256))
ok(fromStackElem(stack[^1], UInt256))
func peekAddress*(stack: EvmStack): EvmResult[Address] =
? ensurePop(stack, 1)
ok(fromStackElem(stack.values[^1], Address))
ok(fromStackElem(stack[^1], Address))
func top*(stack: EvmStack,
value: EvmStackInts | UInt256 | Address | Hash32): EvmResultVoid =
if stack.values.len == 0:
return err(stackErr(StackInsufficient))
toStackElem(value, stack.values[^1])
? ensurePop(stack, 1)
toStackElem(value, stack[^1])
ok()
iterator items*(stack: EvmStack): UInt256 =
for v in stack.values:
yield v
for i in 0..<stack.len:
yield stack[i].data
iterator pairs*(stack: EvmStack): (int, UInt256) =
for i, v in stack.values:
yield (i, v)
for i in 0..<stack.len:
yield (i, stack[i].data)
# ------------------------------------------------------------------------------
# Public functions with less safety
@ -194,63 +212,77 @@ template lsCheck*(stack: EvmStack, expected: int): EvmResultVoid =
func lsTop*(stack: EvmStack,
value: EvmStackInts | UInt256 | Address | Hash32) =
toStackElem(value, stack.values[^1])
toStackElem(value, stack[^1])
func lsTop*(stack: var EvmStack, value: openArray[byte]) =
toStackElem(value, stack.values[^1])
func lsTop*(stack: EvmStack, value: openArray[byte]) =
toStackElem(value, stack[^1])
func lsPeekInt*(stack: EvmStack, i: BackwardsIndex): UInt256 =
fromStackElem(stack.values[i], UInt256)
fromStackElem(stack[i], UInt256)
func lsPeekAddress*(stack: EvmStack, i: BackwardsIndex): Address =
fromStackElem(stack.values[i], Address)
fromStackElem(stack[i], Address)
func lsPeekMemRef*(stack: EvmStack, i: BackwardsIndex): int =
fromStackElem(stack.values[i], UInt256).cleanMemRef
fromStackElem(stack[i], UInt256).cleanMemRef
func lsPeekSafeInt*(stack: EvmStack, i: BackwardsIndex): int =
fromStackElem(stack.values[i], UInt256).safeInt
fromStackElem(stack[i], UInt256).safeInt
func lsPeekTopic*(stack: EvmStack, i: BackwardsIndex): EvmStackBytes32 =
fromStackElem(stack.values[i], EvmStackBytes32)
func lsPeekTopic*(stack: EvmStack, i: BackwardsIndex): Bytes32 =
fromStackElem(stack[i], Bytes32)
func lsShrink*(stack: EvmStack, x: int) =
stack.values.setLen(stack.values.len - x)
stack.len -= x
template binaryOp*(stack: EvmStack, binOp): EvmResultVoid =
if stack.values.len >= 2:
stack.values[^2] = binOp(stack.values[^1], stack.values[^2])
stack.values.setLen(stack.values.len - 1)
let len = stack.len
if len >= 2:
let
l1 = len - 1
l2 = len - 2
stack[l2].data = binOp(stack[l1].data, stack[l2].data)
stack.len = l1
EvmResultVoid.ok()
else:
EvmResultVoid.err(stackErr(StackInsufficient))
template unaryOp*(stack: EvmStack, unOp): EvmResultVoid =
if stack.values.len >= 1:
stack.values[^1] = unOp(stack.values[^1])
let len = stack.len
if len >= 1:
let l1 = len - 1
stack[l1].data = unOp(stack[l1].data)
EvmResultVoid.ok()
else:
EvmResultVoid.err(stackErr(StackInsufficient))
template binaryWithTop*(stack: EvmStack, binOp): EvmResultVoid =
if stack.values.len >= 2:
binOp(stack.values[^2], stack.values[^1], stack.values[^2])
stack.values.setLen(stack.values.len - 1)
let len = stack.len
if len >= 2:
let
l1 = len - 1
l2 = len - 2
binOp(stack[l2].data, stack[l1].data, stack[l2].data)
stack.len = l1
EvmResultVoid.ok()
else:
EvmResultVoid.err(stackErr(StackInsufficient))
template unaryWithTop*(stack: EvmStack, unOp): EvmResultVoid =
if stack.values.len >= 1:
unOp(stack.values[^1], stack.values[^1], toStackElem)
let len = stack.len
if len >= 1:
let l1 = len - 1
unOp(stack[l1], stack[l1].data, toStackElem)
EvmResultVoid.ok()
else:
EvmResultVoid.err(stackErr(StackInsufficient))
template unaryAddress*(stack: EvmStack, unOp): EvmResultVoid =
if stack.values.len >= 1:
let address = fromStackElem(stack.values[^1], Address)
toStackElem(unOp(address), stack.values[^1])
let len = stack.len
if len >= 1:
let l1 = len - 1
let address = fromStackElem(stack[l1], Address)
toStackElem(unOp(address), stack[l1])
EvmResultVoid.ok()
else:
EvmResultVoid.err(stackErr(StackInsufficient))

View File

@ -14,6 +14,8 @@ import
../db/ledger,
../common/[common, evmforks]
export stack, memory
# this import not guarded by `when defined(evmc_enabled)`
# because we want to use evmc types such as evmc_call_kind
# and evmc_flags
@ -76,7 +78,6 @@ type
msg*: Message
memory*: EvmMemory
stack*: EvmStack
returnStack*: seq[int]
gasMeter*: GasMeter
code*: CodeStream
output*: seq[byte]
@ -93,6 +94,8 @@ type
parent*, child*: Computation
continuation*: proc(): EvmResultVoid {.gcsafe, raises: [].}
sysCall*: bool
keepStack*: bool
finalStack*: seq[UInt256]
Error* = ref object
evmcStatus*: evmc_status_code

View File

@ -132,7 +132,7 @@ proc preExecComputation(vmState: BaseVMState, call: CallParams): int64 =
gasRefund
proc setupHost(call: CallParams): TransactionHost =
proc setupHost(call: CallParams, keepStack: bool): TransactionHost =
let vmState = call.vmState
vmState.txCtx = TxContext(
origin : call.origin.get(call.sender),
@ -162,6 +162,8 @@ proc setupHost(call: CallParams): TransactionHost =
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.
@ -179,7 +181,9 @@ proc setupHost(call: CallParams): TransactionHost =
else:
# TODO: Share the underlying data, but only after checking this does not
# cause problems with the database.
if host.vmState.fork >= FkPrague:
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)
@ -190,8 +194,10 @@ proc setupHost(call: CallParams): TransactionHost =
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)
let
cMsg = hostToComputationMessage(host.msg)
host.computation = newComputation(
vmState, call.sysCall, cMsg, code, isPrecompile = isPrecompile, keepStack = keepStack)
host.code = code
else:
@ -202,8 +208,10 @@ proc setupHost(call: CallParams): TransactionHost =
host.input = call.input
host.msg.input_data = host.input[0].addr
let cMsg = hostToComputationMessage(host.msg)
host.computation = newComputation(vmState, call.sysCall, cMsg)
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,
@ -290,7 +298,7 @@ proc finishRunningComputation(
let gasUsed = host.msg.gas.GasInt - gasRemaining
host.vmState.captureEnd(c, c.output, gasUsed, c.errorOpt)
when T is CallResult:
when T is CallResult|DebugCallResult:
# Collecting the result can be unnecessarily expensive when (re)-processing
# transactions
if c.isError:
@ -299,8 +307,10 @@ proc finishRunningComputation(
result.output = system.move(c.output)
result.contractAddress = if call.isCreate: c.msg.contractAddress
else: default(HostAddress)
result.stack = move(c.stack)
result.memory = move(c.memory)
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:
@ -312,7 +322,7 @@ proc finishRunningComputation(
{.error: "Unknown computation output".}
proc runComputation*(call: CallParams, T: type): T =
let host = setupHost(call)
let host = setupHost(call, keepStack = T is DebugCallResult)
prepareToRunComputation(host, call)
when defined(evmc_enabled):

View File

@ -204,6 +204,6 @@ proc txCallEvm*(tx: Transaction,
proc testCallEvm*(tx: Transaction,
sender: Address,
vmState: BaseVMState): CallResult =
vmState: BaseVMState): DebugCallResult =
let call = callParamsForTest(tx, sender, vmState)
runComputation(call, CallResult)
runComputation(call, DebugCallResult)

View File

@ -17,6 +17,8 @@ import
../core/eip7702,
./host_types
export types
type
# Standard call parameters.
CallParams* = object
@ -40,12 +42,14 @@ type
# 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
CallResult* = object of RootObj
error*: string # Something if the call failed.
gasUsed*: GasInt # Gas used by the call.
contractAddress*: Address # Created account (when `isCreate`).
output*: seq[byte] # Output data.
stack*: EvmStack # EVM stack on return (for test only).
DebugCallResult* = object of CallResult
stack*: seq[UInt256] # EVM stack on return (for test only).
memory*: EvmMemory # EVM memory on return (for test only).
template isCreate(tx: Transaction): bool =

View File

@ -129,9 +129,9 @@ proc evmcExecComputation*(host: TransactionHost): EvmcResult =
result = vm.execute(vm, hostInterface.unsafeAddr, hostContext,
evmc_revision(host.vmState.fork.ord), host.msg,
if host.code.len > 0: host.code.bytes[0].unsafeAddr
if host.code != nil and host.code.len > 0: host.code.bytes[0].unsafeAddr
else: nil,
host.code.len.csize_t)
if host.code != nil: host.code.len.csize_t else: 0)
host.showCallReturn(result)

View File

@ -13,7 +13,7 @@ import
stew/ptrops,
stew/saturation_arith,
stint,
../evm/types,
../evm/[types, precompiles],
../evm/interpreter_dispatch,
../utils/utils,
"."/[host_types, host_trace]
@ -34,7 +34,8 @@ proc beforeExecCreateEvmcNested(host: TransactionHost,
value: m.value.fromEvmc,
data: @(makeOpenArray(m.input_data, m.input_size.int))
)
return newComputation(host.vmState, false, childMsg,
return newComputation(host.vmState, false, childMsg, isPrecompile = false,
keepStack = false,
cast[ContractSalt](m.create2_salt))
proc afterExecCreateEvmcNested(host: TransactionHost, child: Computation,
@ -71,7 +72,8 @@ proc beforeExecCallEvmcNested(host: TransactionHost,
data: @(makeOpenArray(m.input_data, m.input_size.int)),
flags: m.flags,
)
return newComputation(host.vmState, false, childMsg)
let isPrecompile = getPrecompile(host.vmState.fork, childMsg.codeAddress).isSome()
newComputation(host.vmState, false, childMsg, isPrecompile = isPrecompile, keepStack = false)
proc afterExecCallEvmcNested(host: TransactionHost, child: Computation,
res: var EvmcResult) {.inline.} =

View File

@ -281,7 +281,7 @@ proc initVMEnv*(network: string): BaseVMState =
BaseVMState.new(parent, header, com)
proc verifyAsmResult(vmState: BaseVMState, boa: Assembler, asmResult: CallResult): bool =
proc verifyAsmResult(vmState: BaseVMState, boa: Assembler, asmResult: DebugCallResult): bool =
let com = vmState.com
if not asmResult.isError:
if boa.success == false:

View File

@ -26,24 +26,20 @@ import
template testPush(value: untyped, expected: untyped): untyped =
privateAccess(EvmStack)
var stack = EvmStack.init()
defer: stack.dispose()
check stack.push(value).isOk
check(stack.values == @[expected])
func toBytes(s: string): seq[byte] =
cast[seq[byte]](s)
func bigEndianToInt(value: openArray[byte]): UInt256 =
result.initFromBytesBE(value)
check(toSeq(stack.items()) == @[expected])
proc runStackTests() =
suite "Stack tests":
test "push only valid":
testPush(0'u, 0.u256)
testPush(UINT_256_MAX, UINT_256_MAX)
testPush("ves".toBytes, "ves".toBytes.bigEndianToInt)
# testPush("ves".toBytes, "ves".toBytes.bigEndianToInt)
test "push does not allow stack to exceed 1024":
var stack = EvmStack.init()
defer: stack.dispose()
for z in 0 ..< 1024:
check stack.push(z.uint).isOk
check(stack.len == 1024)
@ -51,6 +47,7 @@ proc runStackTests() =
test "dup does not allow stack to exceed 1024":
var stack = EvmStack.init()
defer: stack.dispose()
check stack.push(1.u256).isOk
for z in 0 ..< 1023:
check stack.dup(1).isOk
@ -59,6 +56,7 @@ proc runStackTests() =
test "pop returns latest stack item":
var stack = EvmStack.init()
defer: stack.dispose()
for element in @[1'u, 2'u, 3'u]:
check stack.push(element).isOk
check(stack.popInt.get == 3.u256)
@ -66,35 +64,40 @@ proc runStackTests() =
test "swap correct":
privateAccess(EvmStack)
var stack = EvmStack.init()
defer: stack.dispose()
for z in 0 ..< 5:
check stack.push(z.uint).isOk
check(stack.values == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
check stack.swap(3).isOk
check(stack.values == @[0.u256, 4.u256, 2.u256, 3.u256, 1.u256])
check(toSeq(stack.items()) == @[0.u256, 4.u256, 2.u256, 3.u256, 1.u256])
check stack.swap(1).isOk
check(stack.values == @[0.u256, 4.u256, 2.u256, 1.u256, 3.u256])
check(toSeq(stack.items()) == @[0.u256, 4.u256, 2.u256, 1.u256, 3.u256])
test "dup correct":
privateAccess(EvmStack)
var stack = EvmStack.init()
defer: stack.dispose()
for z in 0 ..< 5:
check stack.push(z.uint).isOk
check(stack.values == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
check stack.dup(1).isOk
check(stack.values == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256, 4.u256])
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256, 4.u256])
check stack.dup(5).isOk
check(stack.values == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256, 4.u256, 1.u256])
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256, 4.u256, 1.u256])
test "pop raises InsufficientStack appropriately":
var stack = EvmStack.init()
defer: stack.dispose()
check stack.popInt().error.code == EvmErrorCode.StackInsufficient
test "swap raises InsufficientStack appropriately":
var stack = EvmStack.init()
defer: stack.dispose()
check stack.swap(0).error.code == EvmErrorCode.StackInsufficient
test "dup raises InsufficientStack appropriately":
var stack = EvmStack.init()
defer: stack.dispose()
check stack.dup(0).error.code == EvmErrorCode.StackInsufficient
test "binary operations raises InsufficientStack appropriately":
@ -102,8 +105,9 @@ proc runStackTests() =
# ./tests/fixtures/VMTests/vmArithmeticTest/mulUnderFlow.json
var stack = EvmStack.init()
defer: stack.dispose()
check stack.push(123).isOk
check stack.popInt(2).error.code == EvmErrorCode.StackInsufficient
check stack.binaryOp(`+`).error.code == EvmErrorCode.StackInsufficient
proc memory32: EvmMemory =
result = EvmMemory.init(32)