mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-15 05:26:36 +00:00
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:
parent
b2a4373cc9
commit
b3cb51e89e
@ -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:
|
||||
|
@ -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) =
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
# -------
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.} =
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user