nimbus-eth1/nimbus/evm/stack.nim

294 lines
8.9 KiB
Nim

# Nimbus
# Copyright (c) 2018-2024 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.
# 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/typetraits,
./evm_errors,
./interpreter/utils/utils_numeric
const evmStackSize = 1024
## https://ethereum.org/en/developers/docs/evm/#evm-instructions
type
EvmStack* = ref object
values: ptr EvmStackElement
memory: pointer
len*: int
EvmStackElement = object
data {.align: 32.}: UInt256
EvmStackInts = uint64 | uint | int | GasInt
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 `[]`*(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.data = v.u256
template toStackElem(v: Address, elem: EvmStackElement) =
elem.data.initFromBytesBE(v.data)
template toStackElem(v: Hash32, elem: EvmStackElement) =
elem.data.initFromBytesBE(v.data)
template toStackElem(v: openArray[byte], elem: EvmStackElement) =
elem.data.initFromBytesBE(v)
template fromStackElem(elem: EvmStackElement, _: type UInt256): UInt256 =
elem.data
func fromStackElem(elem: EvmStackElement, _: type Address): Address =
elem.data.to(Bytes32).to(Address)
template fromStackElem(elem: EvmStackElement, _: type Hash32): Hash32 =
Hash32(elem.data.toBytesBE())
template fromStackElem(elem: EvmStackElement, _: type Bytes32): Bytes32 =
elem.data.toBytesBE().to(Bytes32)
func ensurePop(stack: EvmStack, expected: int): EvmResultVoid =
if stack.len < expected:
return err(stackErr(StackInsufficient))
ok()
func popAux(stack: EvmStack, T: type): EvmResult[T] =
? ensurePop(stack, 1)
stack.len -= 1
ok(fromStackElem(stack[stack.len], T))
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
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 popInt*(stack: EvmStack): EvmResult[UInt256] =
popAux(stack, UInt256)
func popAddress*(stack: EvmStack): EvmResult[Address] =
popAux(stack, Address)
func pop*(stack: EvmStack): EvmResult[void] =
? ensurePop(stack, 1)
stack.len -= 1
ok()
proc init*(_: type EvmStack): EvmStack =
let memory = c_malloc(evmStackSize * sizeof(EvmStackElement) + 31)
EvmStack(
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,
)
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: EvmStack, position: int): EvmResultVoid =
## Push copy of item at `top - position`
if position in 1 .. stack.len:
stack.push(stack[^position])
else:
err(stackErr(StackInsufficient))
func peek*(stack: EvmStack): EvmResult[UInt256] =
? ensurePop(stack, 1)
ok(fromStackElem(stack[^1], UInt256))
func peekSafeInt*(stack: EvmStack): EvmResult[int] =
? 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[i], T))
func peekInt*(stack: EvmStack): EvmResult[UInt256] =
? ensurePop(stack, 1)
ok(fromStackElem(stack[^1], UInt256))
func peekAddress*(stack: EvmStack): EvmResult[Address] =
? ensurePop(stack, 1)
ok(fromStackElem(stack[^1], Address))
func top*(stack: EvmStack,
value: EvmStackInts | UInt256 | Address | Hash32): EvmResultVoid =
? ensurePop(stack, 1)
toStackElem(value, stack[^1])
ok()
iterator items*(stack: EvmStack): UInt256 =
for i in 0..<stack.len:
yield stack[i].data
iterator pairs*(stack: EvmStack): (int, UInt256) =
for i in 0..<stack.len:
yield (i, stack[i].data)
# ------------------------------------------------------------------------------
# Public functions with less safety
# ------------------------------------------------------------------------------
template lsCheck*(stack: EvmStack, expected: int): EvmResultVoid =
ensurePop(stack, expected)
func lsTop*(stack: EvmStack,
value: EvmStackInts | UInt256 | Address | Hash32) =
toStackElem(value, stack[^1])
func lsTop*(stack: EvmStack, value: openArray[byte]) =
toStackElem(value, stack[^1])
func lsPeekInt*(stack: EvmStack, i: BackwardsIndex): UInt256 =
fromStackElem(stack[i], UInt256)
func lsPeekAddress*(stack: EvmStack, i: BackwardsIndex): Address =
fromStackElem(stack[i], Address)
func lsPeekMemRef*(stack: EvmStack, i: BackwardsIndex): int =
fromStackElem(stack[i], UInt256).cleanMemRef
func lsPeekSafeInt*(stack: EvmStack, i: BackwardsIndex): int =
fromStackElem(stack[i], UInt256).safeInt
func lsPeekTopic*(stack: EvmStack, i: BackwardsIndex): Bytes32 =
fromStackElem(stack[i], Bytes32)
func lsShrink*(stack: EvmStack, x: int) =
stack.len -= x
template binaryOp*(stack: EvmStack, binOp): EvmResultVoid =
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 =
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 =
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 =
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 =
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))