Jacek Sieka b3cb51e89e
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.
2024-11-30 10:07:10 +01:00

492 lines
14 KiB
Nim

# Nimbus
# Copyright (c) 2021-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.
## EVM Opcode Handlers: Stack, Memory, Storage And Flow Operations
## ===============================================================
##
{.push raises: [].}
import
../../evm_errors,
../../code_stream,
../../computation,
../../memory,
../../stack,
../../types,
../gas_meter,
../gas_costs,
../op_codes,
./oph_defs,
./oph_helpers,
eth/common,
stint
when not defined(evmc_enabled):
import
../../state,
../../../db/ledger
else:
import
../evmc_gas_costs
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
when evmc_enabled:
proc sstoreEvmc(c: Computation, slot, newValue: UInt256, coldAccess = 0.GasInt): EvmResultVoid =
let
status = c.host.setStorage(c.msg.contractAddress, slot, newValue)
res = ForkToSstoreCost[c.fork][status]
gasCost = res.gasCost.GasInt + coldAccess
? c.opcodeGasCost(Sstore, gasCost, "SSTORE")
c.gasMeter.refundGas(res.gasRefund)
ok()
else:
proc sstoreImpl(c: Computation, slot, newValue: UInt256): EvmResultVoid =
let
currentValue = c.getStorage(slot)
gasParam = GasParamsSs(
currentValue: currentValue)
res = c.gasCosts[Sstore].ss_handler(newValue, gasParam)
? c.opcodeGasCost(Sstore, res.gasCost, "SSTORE")
c.gasMeter.refundGas(res.gasRefund)
c.vmState.mutateStateDB:
db.setStorage(c.msg.contractAddress, slot, newValue)
ok()
proc sstoreNetGasMeteringImpl(c: Computation; slot, newValue: UInt256, coldAccess = 0.GasInt): EvmResultVoid =
let
stateDB = c.vmState.readOnlyStateDB
currentValue = c.getStorage(slot)
gasParam = GasParamsSs(
currentValue: currentValue,
originalValue: stateDB.getCommittedStorage(c.msg.contractAddress, slot))
res = c.gasCosts[Sstore].ss_handler(newValue, gasParam)
? c.opcodeGasCost(Sstore, res.gasCost + coldAccess, "SSTORE")
c.gasMeter.refundGas(res.gasRefund)
c.vmState.mutateStateDB:
db.setStorage(c.msg.contractAddress, slot, newValue)
ok()
template sstoreEvmcOrSstore(cpt, slot, newValue: untyped): auto =
when evmc_enabled:
sstoreEvmc(cpt, slot, newValue, 0.GasInt)
else:
sstoreImpl(cpt, slot, newValue)
template sstoreEvmcOrNetGasMetering(cpt, slot, newValue: untyped, coldAccess = 0.GasInt): auto =
when evmc_enabled:
sstoreEvmc(cpt, slot, newValue, coldAccess)
else:
sstoreNetGasMeteringImpl(cpt, slot, newValue, coldAccess)
func jumpImpl(c: Computation; jumpTarget: UInt256): EvmResultVoid =
if jumpTarget >= c.code.len.u256:
return err(opErr(InvalidJumpDest))
let jt = jumpTarget.truncate(int)
c.code.pc = jt
let nextOpcode = c.code.peek
if nextOpcode != JumpDest:
return err(opErr(InvalidJumpDest))
# Jump destination must be a valid opcode
if not c.code.isValidOpcode(jt):
return err(opErr(InvalidJumpDest))
ok()
# ------------------------------------------------------------------------------
# Private, op handlers implementation
# ------------------------------------------------------------------------------
proc popOp(cpt: VmCpt): EvmResultVoid =
## 0x50, Remove item from stack.
cpt.stack.popInt.isOkOr:
return err(error)
ok()
proc mloadOp(cpt: VmCpt): EvmResultVoid =
## 0x51, Load word from memory
? cpt.stack.lsCheck(1)
let memPos = cpt.stack.lsPeekMemRef(^1)
? cpt.opcodeGasCost(Mload,
cpt.gasCosts[Mload].m_handler(cpt.memory.len, memPos, 32),
reason = "MLOAD: GasVeryLow + memory expansion")
cpt.memory.extend(memPos, 32)
cpt.stack.lsTop cpt.memory.read32Bytes(memPos)
ok()
proc mstoreOp(cpt: VmCpt): EvmResultVoid =
## 0x52, Save word to memory
? cpt.stack.lsCheck(2)
let
memPos = cpt.stack.lsPeekMemRef(^1)
value = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? cpt.opcodeGasCost(Mstore,
cpt.gasCosts[Mstore].m_handler(cpt.memory.len, memPos, 32),
reason = "MSTORE: GasVeryLow + memory expansion")
cpt.memory.extend(memPos, 32)
cpt.memory.write(memPos, value.toBytesBE)
proc mstore8Op(cpt: VmCpt): EvmResultVoid =
## 0x53, Save byte to memory
? cpt.stack.lsCheck(2)
let
memPos = cpt.stack.lsPeekMemRef(^1)
value = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? cpt.opcodeGasCost(Mstore8,
cpt.gasCosts[Mstore8].m_handler(cpt.memory.len, memPos, 1),
reason = "MSTORE8: GasVeryLow + memory expansion")
cpt.memory.extend(memPos, 1)
cpt.memory.write(memPos, value.toByteArrayBE[31])
# -------
proc sloadOp(cpt: VmCpt): EvmResultVoid =
## 0x54, Load word from storage.
template sload256(top, slot, conv) =
conv(cpt.getStorage(slot), top)
cpt.stack.unaryWithTop(sload256)
proc sloadEIP2929Op(cpt: VmCpt): EvmResultVoid =
## 0x54, EIP2929: Load word from storage for Berlin and later
template sloadEIP2929(top, slot, conv) =
let gasCost = cpt.gasEip2929AccountCheck(cpt.msg.contractAddress, slot)
? cpt.opcodeGasCost(Sload, gasCost, reason = "sloadEIP2929")
conv(cpt.getStorage(slot), top)
cpt.stack.unaryWithTop(sloadEIP2929)
# -------
proc sstoreOp(cpt: VmCpt): EvmResultVoid =
## 0x55, Save word to storage.
? cpt.stack.lsCheck(2)
let
slot = cpt.stack.lsPeekInt(^1)
newValue = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? checkInStaticContext(cpt)
sstoreEvmcOrSstore(cpt, slot, newValue)
proc sstoreEIP1283Op(cpt: VmCpt): EvmResultVoid =
## 0x55, EIP1283: sstore for Constantinople and later
? cpt.stack.lsCheck(2)
let
slot = cpt.stack.lsPeekInt(^1)
newValue = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? checkInStaticContext(cpt)
sstoreEvmcOrNetGasMetering(cpt, slot, newValue)
proc sstoreEIP2200Op(cpt: VmCpt): EvmResultVoid =
## 0x55, EIP2200: sstore for Istanbul and later
? cpt.stack.lsCheck(2)
let
slot = cpt.stack.lsPeekInt(^1)
newValue = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? checkInStaticContext(cpt)
const SentryGasEIP2200 = 2300
if cpt.gasMeter.gasRemaining <= SentryGasEIP2200:
return err(opErr(OutOfGas))
sstoreEvmcOrNetGasMetering(cpt, slot, newValue)
proc sstoreEIP2929Op(cpt: VmCpt): EvmResultVoid =
## 0x55, EIP2929: sstore for Berlin and later
? cpt.stack.lsCheck(2)
let
slot = cpt.stack.lsPeekInt(^1)
newValue = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
? checkInStaticContext(cpt)
# Minimum gas required to be present for an SSTORE call, not consumed
const SentryGasEIP2200 = 2300
if cpt.gasMeter.gasRemaining <= SentryGasEIP2200:
return err(opErr(OutOfGas))
var coldAccessGas = 0.GasInt
when evmc_enabled:
if cpt.host.accessStorage(cpt.msg.contractAddress, slot) == EVMC_ACCESS_COLD:
coldAccessGas = ColdSloadCost
else:
cpt.vmState.mutateStateDB:
if not db.inAccessList(cpt.msg.contractAddress, slot):
db.accessList(cpt.msg.contractAddress, slot)
coldAccessGas = ColdSloadCost
sstoreEvmcOrNetGasMetering(cpt, slot, newValue, coldAccessGas)
# -------
proc jumpOp(cpt: VmCpt): EvmResultVoid =
## 0x56, Alter the program counter
let jumpTarget = ? cpt.stack.popInt()
cpt.jumpImpl(jumpTarget)
proc jumpIOp(cpt: VmCpt): EvmResultVoid =
## 0x57, Conditionally alter the program counter.
? cpt.stack.lsCheck(2)
let
jumpTarget = cpt.stack.lsPeekInt(^1)
testedValue = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
if testedValue.isZero:
return ok()
cpt.jumpImpl(jumpTarget)
proc pcOp(cpt: VmCpt): EvmResultVoid =
## 0x58, Get the value of the program counter prior to the increment
## corresponding to this instruction.
cpt.stack.push max(cpt.code.pc - 1, 0)
proc msizeOp(cpt: VmCpt): EvmResultVoid =
## 0x59, Get the size of active memory in bytes.
cpt.stack.push cpt.memory.len
proc gasOp(cpt: VmCpt): EvmResultVoid =
## 0x5a, Get the amount of available gas, including the corresponding
## reduction for the cost of this instruction.
cpt.stack.push cpt.gasMeter.gasRemaining
proc jumpDestOp(cpt: VmCpt): EvmResultVoid =
## 0x5b, Mark a valid destination for jumps. This operation has no effect
## on machine state during execution.
ok()
proc tloadOp(cpt: VmCpt): EvmResultVoid =
## 0x5c, Load word from transient storage.
? cpt.stack.lsCheck(1)
let
slot = cpt.stack.lsPeekInt(^1)
val = cpt.getTransientStorage(slot)
cpt.stack.lsTop val
ok()
proc tstoreOp(cpt: VmCpt): EvmResultVoid =
## 0x5d, Save word to transient storage.
? cpt.checkInStaticContext()
? cpt.stack.lsCheck(2)
let
slot = cpt.stack.lsPeekInt(^1)
val = cpt.stack.lsPeekInt(^2)
cpt.stack.lsShrink(2)
cpt.setTransientStorage(slot, val)
ok()
proc mCopyOp(cpt: VmCpt): EvmResultVoid =
## 0x5e, Copy memory
? cpt.stack.lsCheck(3)
let
dstPos = cpt.stack.lsPeekMemRef(^1)
srcPos = cpt.stack.lsPeekMemRef(^2)
len = cpt.stack.lsPeekMemRef(^3)
cpt.stack.lsShrink(3)
? cpt.opcodeGasCost(Mcopy,
cpt.gasCosts[Mcopy].m_handler(cpt.memory.len, max(dstPos, srcPos), len),
reason = "Mcopy fee")
cpt.memory.copy(dstPos, srcPos, len)
ok()
# ------------------------------------------------------------------------------
# Public, op exec table entries
# ------------------------------------------------------------------------------
const
VmOpExecMemory*: seq[VmOpExec] = @[
(opCode: Pop, ## x50, Remove item from stack
forks: VmOpAllForks,
name: "pop",
info: "Remove item from stack",
exec: VmOpFn popOp),
(opCode: Mload, ## 0x51, Load word from memory
forks: VmOpAllForks,
name: "mload",
info: "Load word from memory",
exec: mloadOp),
(opCode: Mstore, ## 0x52, Save word to memory
forks: VmOpAllForks,
name: "mstore",
info: "Save word to memory",
exec: mstoreOp),
(opCode: Mstore8, ## 0x53, Save byte to memory
forks: VmOpAllForks,
name: "mstore8",
info: "Save byte to memory",
exec: mstore8Op),
(opCode: Sload, ## 0x54, Load word from storage
forks: VmOpAllForks - VmOpBerlinAndLater,
name: "sload",
info: "Load word from storage",
exec: sloadOp),
(opCode: Sload, ## 0x54, sload for Berlin and later
forks: VmOpBerlinAndLater,
name: "sloadEIP2929",
info: "EIP2929: sload for Berlin and later",
exec: sloadEIP2929Op),
(opCode: Sstore, ## 0x55, Save word
forks: VmOpAllForks - VmOpConstantinopleAndLater,
name: "sstore",
info: "Save word to storage",
exec: sstoreOp),
(opCode: Sstore, ## 0x55, sstore for Constantinople and later
forks: VmOpConstantinopleAndLater - VmOpPetersburgAndLater,
name: "sstoreEIP1283",
info: "EIP1283: sstore for Constantinople and later",
exec: sstoreEIP1283Op),
(opCode: Sstore, ## 0x55, sstore for Petersburg and later
forks: VmOpPetersburgAndLater - VmOpIstanbulAndLater,
name: "sstore",
info: "sstore for Constantinople and later",
exec: sstoreOp),
(opCode: Sstore, ## 0x55, sstore for Istanbul and later
forks: VmOpIstanbulAndLater - VmOpBerlinAndLater,
name: "sstoreEIP2200",
info: "EIP2200: sstore for Istanbul and later",
exec: sstoreEIP2200Op),
(opCode: Sstore, ## 0x55, sstore for Berlin and later
forks: VmOpBerlinAndLater,
name: "sstoreEIP2929",
info: "EIP2929: sstore for Istanbul and later",
exec: sstoreEIP2929Op),
(opCode: Jump, ## 0x56, Jump
forks: VmOpAllForks,
name: "jump",
info: "Alter the program counter",
exec: jumpOp),
(opCode: JumpI, ## 0x57, Conditional jump
forks: VmOpAllForks,
name: "jumpI",
info: "Conditionally alter the program counter",
exec: jumpIOp),
(opCode: Pc, ## 0x58, Program counter prior to instruction
forks: VmOpAllForks,
name: "pc",
info: "Get the value of the program counter prior to the increment "&
"corresponding to this instruction",
exec: pcOp),
(opCode: Msize, ## 0x59, Memory size
forks: VmOpAllForks,
name: "msize",
info: "Get the size of active memory in bytes",
exec: msizeOp),
(opCode: Gas, ## 0x5a, Get available gas
forks: VmOpAllForks,
name: "gas",
info: "Get the amount of available gas, including the corresponding "&
"reduction for the cost of this instruction",
exec: gasOp),
(opCode: JumpDest, ## 0x5b, Mark jump target. This operation has no effect
## on machine state during execution
forks: VmOpAllForks,
name: "jumpDest",
info: "Mark a valid destination for jumps",
exec: jumpDestOp),
(opCode: Tload, ## 0x5c, Load word from transient storage.
forks: VmOpCancunAndLater,
name: "tLoad",
info: "Load word from transient storage",
exec: tloadOp),
(opCode: Tstore, ## 0x5d, Save word to transient storage.
forks: VmOpCancunAndLater,
name: "tStore",
info: "Save word to transient storage",
exec: tstoreOp),
(opCode: Mcopy, ## 0x5e, Copy memory
forks: VmOpCancunAndLater,
name: "MCopy",
info: "Copy memory",
exec: mCopyOp)]
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------