mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-14 14:24:32 +00:00
b3cb51e89e
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.
383 lines
14 KiB
Nim
383 lines
14 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.
|
|
|
|
import
|
|
std/[importutils, sequtils],
|
|
unittest2,
|
|
eth/common/[keys, transaction_utils],
|
|
../nimbus/common,
|
|
../nimbus/transaction,
|
|
../nimbus/evm/types,
|
|
../nimbus/evm/state,
|
|
../nimbus/evm/evm_errors,
|
|
../nimbus/evm/stack,
|
|
../nimbus/evm/memory,
|
|
../nimbus/evm/code_stream,
|
|
../nimbus/evm/internals,
|
|
../nimbus/constants,
|
|
../nimbus/core/pow/header,
|
|
../nimbus/db/ledger,
|
|
../nimbus/transaction/call_evm
|
|
|
|
template testPush(value: untyped, expected: untyped): untyped =
|
|
privateAccess(EvmStack)
|
|
var stack = EvmStack.init()
|
|
defer: stack.dispose()
|
|
check stack.push(value).isOk
|
|
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)
|
|
|
|
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)
|
|
check stack.push(1025).error.code == EvmErrorCode.StackFull
|
|
|
|
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
|
|
check(stack.len == 1024)
|
|
check stack.dup(1).error.code == EvmErrorCode.StackFull
|
|
|
|
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)
|
|
|
|
test "swap correct":
|
|
privateAccess(EvmStack)
|
|
var stack = EvmStack.init()
|
|
defer: stack.dispose()
|
|
for z in 0 ..< 5:
|
|
check stack.push(z.uint).isOk
|
|
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
|
|
check stack.swap(3).isOk
|
|
check(toSeq(stack.items()) == @[0.u256, 4.u256, 2.u256, 3.u256, 1.u256])
|
|
check stack.swap(1).isOk
|
|
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(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256])
|
|
check stack.dup(1).isOk
|
|
check(toSeq(stack.items()) == @[0.u256, 1.u256, 2.u256, 3.u256, 4.u256, 4.u256])
|
|
check stack.dup(5).isOk
|
|
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":
|
|
# https://github.com/status-im/nimbus/issues/31
|
|
# ./tests/fixtures/VMTests/vmArithmeticTest/mulUnderFlow.json
|
|
|
|
var stack = EvmStack.init()
|
|
defer: stack.dispose()
|
|
check stack.push(123).isOk
|
|
check stack.binaryOp(`+`).error.code == EvmErrorCode.StackInsufficient
|
|
|
|
proc memory32: EvmMemory =
|
|
result = EvmMemory.init(32)
|
|
|
|
proc memory128: EvmMemory =
|
|
result = EvmMemory.init(123)
|
|
|
|
proc runMemoryTests() =
|
|
suite "Memory tests":
|
|
test "write":
|
|
var mem = memory32()
|
|
# Test that write creates 32byte string == value padded with zeros
|
|
check mem.write(startPos = 0, value = @[1.byte, 0.byte, 1.byte, 0.byte]).isOk
|
|
check(mem.bytes == @[1.byte, 0.byte, 1.byte, 0.byte].concat(repeat(0.byte, 28)))
|
|
|
|
test "write rejects values beyond memory size":
|
|
var mem = memory128()
|
|
check mem.write(startPos = 128, value = @[1.byte, 0.byte, 1.byte, 0.byte]).error.code == EvmErrorCode.MemoryFull
|
|
check mem.write(startPos = 128, value = 1.byte).error.code == EvmErrorCode.MemoryFull
|
|
|
|
test "extends appropriately extends memory":
|
|
var mem = EvmMemory.init()
|
|
# Test extends to 32 byte array: 0 < (start_position + size) <= 32
|
|
mem.extend(startPos = 0, size = 10)
|
|
check(mem.bytes == repeat(0.byte, 32))
|
|
# Test will extend past length if params require: 32 < (start_position + size) <= 64
|
|
mem.extend(startPos = 28, size = 32)
|
|
check(mem.bytes == repeat(0.byte, 64))
|
|
# Test won't extend past length unless params require: 32 < (start_position + size) <= 64
|
|
mem.extend(startPos = 48, size = 10)
|
|
check(mem.bytes == repeat(0.byte, 64))
|
|
|
|
test "read returns correct bytes":
|
|
var mem = memory32()
|
|
check mem.write(startPos = 5, value = @[1.byte, 0.byte, 1.byte, 0.byte]).isOk
|
|
check(@(mem.read(startPos = 5, size = 4)) == @[1.byte, 0.byte, 1.byte, 0.byte])
|
|
check(@(mem.read(startPos = 6, size = 4)) == @[0.byte, 1.byte, 0.byte, 0.byte])
|
|
check(@(mem.read(startPos = 1, size = 3)) == @[0.byte, 0.byte, 0.byte])
|
|
|
|
proc runCodeStreamTests() =
|
|
suite "Codestream tests":
|
|
test "accepts bytes":
|
|
let codeStream = CodeStream.init("\x01")
|
|
check(codeStream.len == 1)
|
|
|
|
test "next returns the correct opcode":
|
|
var codeStream = CodeStream.init("\x01\x02\x30")
|
|
check(codeStream.next == Op.ADD)
|
|
check(codeStream.next == Op.MUL)
|
|
check(codeStream.next == Op.ADDRESS)
|
|
|
|
test "peek returns next opcode without changing location":
|
|
var codeStream = CodeStream.init("\x01\x02\x30")
|
|
check(codeStream.pc == 0)
|
|
check(codeStream.peek == Op.ADD)
|
|
check(codeStream.pc == 0)
|
|
check(codeStream.next == Op.ADD)
|
|
check(codeStream.pc == 1)
|
|
check(codeStream.peek == Op.MUL)
|
|
check(codeStream.pc == 1)
|
|
|
|
test "stop opcode is returned when end reached":
|
|
var codeStream = CodeStream.init("\x01\x02")
|
|
discard codeStream.next
|
|
discard codeStream.next
|
|
check(codeStream.next == Op.STOP)
|
|
|
|
test "[] returns opcode":
|
|
let codeStream = CodeStream.init("\x01\x02\x30")
|
|
check(codeStream[0] == Op.ADD)
|
|
check(codeStream[1] == Op.MUL)
|
|
check(codeStream[2] == Op.ADDRESS)
|
|
|
|
test "isValidOpcode invalidates after PUSHXX":
|
|
var codeStream = CodeStream.init("\x02\x60\x02\x04")
|
|
check(codeStream.isValidOpcode(0))
|
|
check(codeStream.isValidOpcode(1))
|
|
check(not codeStream.isValidOpcode(2))
|
|
check(codeStream.isValidOpcode(3))
|
|
check(not codeStream.isValidOpcode(4))
|
|
|
|
test "isValidOpcode 0":
|
|
var codeStream = CodeStream.init(@[2.byte, 3.byte, 0x72.byte].concat(repeat(4.byte, 32)).concat(@[5.byte]))
|
|
# valid: 0 - 2 :: 22 - 35
|
|
# invalid: 3-21 (PUSH19) :: 36+ (too long)
|
|
check(codeStream.isValidOpcode(0))
|
|
check(codeStream.isValidOpcode(1))
|
|
check(codeStream.isValidOpcode(2))
|
|
check(not codeStream.isValidOpcode(3))
|
|
check(not codeStream.isValidOpcode(21))
|
|
check(codeStream.isValidOpcode(22))
|
|
check(codeStream.isValidOpcode(35))
|
|
check(not codeStream.isValidOpcode(36))
|
|
|
|
test "isValidOpcode 1":
|
|
let test = @[2.byte, 3.byte, 0x7d.byte].concat(repeat(4.byte, 32)).concat(@[5.byte, 0x7e.byte]).concat(repeat(4.byte, 35)).concat(@[1.byte, 0x61.byte, 1.byte, 1.byte, 1.byte])
|
|
var codeStream = CodeStream.init(test)
|
|
# valid: 0 - 2 :: 33 - 36 :: 68 - 73 :: 76
|
|
# invalid: 3 - 32 (PUSH30) :: 37 - 67 (PUSH31) :: 74, 75 (PUSH2) :: 77+ (too long)
|
|
check(codeStream.isValidOpcode(0))
|
|
check(codeStream.isValidOpcode(1))
|
|
check(codeStream.isValidOpcode(2))
|
|
check(not codeStream.isValidOpcode(3))
|
|
check(not codeStream.isValidOpcode(32))
|
|
check(codeStream.isValidOpcode(33))
|
|
check(codeStream.isValidOpcode(36))
|
|
check(not codeStream.isValidOpcode(37))
|
|
check(not codeStream.isValidOpcode(67))
|
|
check(codeStream.isValidOpcode(68))
|
|
check(codeStream.isValidOpcode(71))
|
|
check(codeStream.isValidOpcode(72))
|
|
check(codeStream.isValidOpcode(73))
|
|
check(not codeStream.isValidOpcode(74))
|
|
check(not codeStream.isValidOpcode(75))
|
|
check(codeStream.isValidOpcode(76))
|
|
check(not codeStream.isValidOpcode(77))
|
|
|
|
test "right number of bytes invalidates":
|
|
var codeStream = CodeStream.init("\x02\x03\x60\x02\x02")
|
|
check(codeStream.isValidOpcode(0))
|
|
check(codeStream.isValidOpcode(1))
|
|
check(codeStream.isValidOpcode(2))
|
|
check(not codeStream.isValidOpcode(3))
|
|
check(codeStream.isValidOpcode(4))
|
|
check(not codeStream.isValidOpcode(5))
|
|
|
|
|
|
proc initGasMeter(startGas: GasInt): GasMeter = result.init(startGas)
|
|
|
|
proc gasMeters: seq[GasMeter] =
|
|
@[initGasMeter(10), initGasMeter(100), initGasMeter(999)]
|
|
|
|
template runTest(body: untyped) =
|
|
var res = gasMeters()
|
|
for gasMeter {.inject.} in res.mitems:
|
|
let StartGas {.inject.} = gasMeter.gasRemaining
|
|
body
|
|
|
|
proc runGasMeterTests() =
|
|
suite "GasMeter tests":
|
|
test "consume spends":
|
|
runTest:
|
|
check(gasMeter.gasRemaining == StartGas)
|
|
let consume = StartGas
|
|
check gasMeter.consumeGas(consume, "0").isOk
|
|
check(gasMeter.gasRemaining - (StartGas - consume) == 0)
|
|
|
|
test "consume errors":
|
|
runTest:
|
|
check(gasMeter.gasRemaining == StartGas)
|
|
check gasMeter.consumeGas(StartGas + 1, "").error.code == EvmErrorCode.OutOfGas
|
|
|
|
test "return refund works correctly":
|
|
runTest:
|
|
check(gasMeter.gasRemaining == StartGas)
|
|
check(gasMeter.gasRefunded == 0)
|
|
check gasMeter.consumeGas(5, "").isOk
|
|
check(gasMeter.gasRemaining == StartGas - 5)
|
|
gasMeter.returnGas(5)
|
|
check(gasMeter.gasRemaining == StartGas)
|
|
gasMeter.refundGas(5)
|
|
check(gasMeter.gasRefunded == 5)
|
|
|
|
proc runMiscTests() =
|
|
suite "Misc test suite":
|
|
test "calcGasLimitEIP1559":
|
|
type
|
|
GLT = object
|
|
limit: GasInt
|
|
max : GasInt
|
|
min : GasInt
|
|
|
|
const testData = [
|
|
GLT(limit: 20000000, max: 20019530, min: 19980470),
|
|
GLT(limit: 40000000, max: 40039061, min: 39960939)
|
|
]
|
|
|
|
for x in testData:
|
|
# Increase
|
|
var have = calcGasLimit1559(x.limit, 2*x.limit)
|
|
var want = x.max
|
|
check have == want
|
|
|
|
# Decrease
|
|
have = calcGasLimit1559(x.limit, 0)
|
|
want = x.min
|
|
check have == want
|
|
|
|
# Small decrease
|
|
have = calcGasLimit1559(x.limit, x.limit-1)
|
|
want = x.limit-1
|
|
check have == want
|
|
|
|
# Small increase
|
|
have = calcGasLimit1559(x.limit, x.limit+1)
|
|
want = x.limit+1
|
|
check have == want
|
|
|
|
# No change
|
|
have = calcGasLimit1559(x.limit, x.limit)
|
|
want = x.limit
|
|
check have == want
|
|
|
|
const
|
|
data = [0x5b.uint8, 0x5a, 0x5a, 0x30, 0x30, 0x30, 0x30, 0x72, 0x00, 0x00, 0x00, 0x58,
|
|
0x58, 0x24, 0x58, 0x58, 0x3a, 0x19, 0x75, 0x75, 0x2e, 0x2e, 0x2e, 0x2e,
|
|
0xec, 0x9f, 0x69, 0x67, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x6c, 0x5a, 0x32,
|
|
0x07, 0xf4, 0x75, 0x75, 0xf5, 0x75, 0x75, 0x75, 0x7f, 0x5b, 0xd9, 0x32,
|
|
0x5a, 0x07, 0x19, 0x34, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
|
|
0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
|
|
0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0xec,
|
|
0x9f, 0x69, 0x67, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x6c, 0xfc, 0xf7, 0xfc,
|
|
0xfc, 0xfc, 0xfc, 0xf4, 0x03, 0x03, 0x81, 0x81, 0x81, 0xfb, 0x7a, 0x30,
|
|
0x80, 0x3d, 0x59, 0x59, 0x59, 0x59, 0x81, 0x00, 0x59, 0x2f, 0x45, 0x30,
|
|
0x32, 0xf4, 0x5d, 0x5b, 0x37, 0x19]
|
|
|
|
codeAddress = address"000000000000000000000000636f6e7472616374"
|
|
coinbase = address"4444588443C3a91288c5002483449Aba1054192b"
|
|
|
|
proc runTestOverflow() =
|
|
test "GasCall unhandled overflow":
|
|
let header = Header(
|
|
stateRoot: EMPTY_ROOT_HASH,
|
|
number: 1150000'u64,
|
|
coinBase: coinbase,
|
|
gasLimit: 30000000,
|
|
timeStamp: EthTime(123456),
|
|
)
|
|
|
|
let com = CommonRef.new(
|
|
newCoreDbRef(DefaultDbMemory),
|
|
config = chainConfigForNetwork(MainNet)
|
|
)
|
|
|
|
let s = BaseVMState.new(
|
|
header,
|
|
header,
|
|
com,
|
|
)
|
|
|
|
s.stateDB.setCode(codeAddress, @data)
|
|
let unsignedTx = Transaction(
|
|
txType: TxLegacy,
|
|
nonce: 0,
|
|
chainId: MainNet.ChainId,
|
|
gasPrice: 0.GasInt,
|
|
gasLimit: 30000000,
|
|
to: Opt.some codeAddress,
|
|
value: 0.u256,
|
|
payload: @data
|
|
)
|
|
|
|
let privateKey = PrivateKey.fromHex("0000000000000000000000000000000000000000000000000000001000000000")[]
|
|
let tx = signTransaction(unsignedTx, privateKey, false)
|
|
let res = testCallEvm(tx, tx.recoverSender().expect("valid signature"), s)
|
|
|
|
when defined(evmc_enabled):
|
|
check res.error == "EVMC_FAILURE"
|
|
else:
|
|
# After gasCall values always on positive, this test become OOG
|
|
check res.error == "Opcode Dispatch Error: OutOfGas, depth=1"
|
|
|
|
proc evmSupportMain*() =
|
|
runStackTests()
|
|
runMemoryTests()
|
|
runCodeStreamTests()
|
|
runGasMeterTests()
|
|
runMiscTests()
|
|
runTestOverflow()
|
|
|
|
when isMainModule:
|
|
evmSupportMain()
|