@tokamak-zk-evm/synthesizer
Version:
Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation
289 lines (257 loc) • 7.81 kB
text/typescript
import { Hardfork } from '@synthesizer-libs/common'
import {
BIGINT_0,
BIGINT_1,
BIGINT_160,
BIGINT_2,
BIGINT_32,
BIGINT_64,
BIGINT_NEG1,
bytesToHex,
createAddressFromBigInt,
equalsBytes,
setLengthLeft,
setLengthRight,
} from "@synthesizer-libs/util"
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { EvmError } from '../exceptions.js'
import { copyMemoryRegion } from '../tokamak/pointers/index.js'
import type { ERROR } from '../exceptions.js'
import type { RunState } from '../interpreter.js'
import type { Common } from '@synthesizer-libs/common'
import type { Address } from "@synthesizer-libs/util"
const MASK_160 = (BIGINT_1 << BIGINT_160) - BIGINT_1
/**
* Create an address from a stack item (256 bit integer).
* This wrapper ensures that the value is masked to 160 bits.
* @param value 160-bit integer
*/
export function createAddressFromStackBigInt(value: bigint): Address {
const maskedValue = value & MASK_160
return createAddressFromBigInt(maskedValue)
}
/**
* Proxy function for @synthesizer-libs/util's setLengthLeft, except it returns a zero
* length Uint8Array in case the Uint8Array is full of zeros.
* @param value Uint8Array which we want to pad
*/
export function setLengthLeftStorage(value: Uint8Array) {
if (equalsBytes(value, new Uint8Array(value.length))) {
// return the empty Uint8Array (the value is zero)
return new Uint8Array(0)
} else {
return setLengthLeft(value, 32)
}
}
/**
* Wraps error message as EvmError
*/
export function trap(err: string) {
// TODO: facilitate extra data along with errors
throw new EvmError(err as ERROR)
}
/**
* Error message helper - generates location string
*/
export function describeLocation(runState: RunState): string {
const keccakFunction = runState.interpreter._evm.common.customCrypto.keccak256 ?? keccak256
const hash = bytesToHex(keccakFunction(runState.interpreter.getCode()))
const address = runState.interpreter.getAddress().toString()
const pc = runState.programCounter - 1
return `${hash}/${address}:${pc}`
}
/**
* Find Ceil(a / b)
*
* @param {bigint} a
* @param {bigint} b
* @return {bigint}
*/
export function divCeil(a: bigint, b: bigint): bigint {
const div = a / b
const modulus = mod(a, b)
// Fast case - exact division
if (modulus === BIGINT_0) return div
// Round up
return div < BIGINT_0 ? div - BIGINT_1 : div + BIGINT_1
}
/**
* Returns an overflow-safe slice of an array. It right-pads
* the data with zeros to `length`.
*/
export function getDataSlice(data: Uint8Array, offset: bigint, length: bigint): Uint8Array {
const len = BigInt(data.length)
if (offset > len) {
offset = len
}
let end = offset + length
if (end > len) {
end = len
}
data = data.subarray(Number(offset), Number(end))
// Right-pad with zeros to fill dataLength bytes
data = setLengthRight(data, Number(length))
return data
}
/**
* Get full opcode name from its name and code.
*
* @param code Integer code of opcode.
* @param name Short name of the opcode.
* @returns Full opcode name
*/
export function getFullname(code: number, name: string): string {
switch (name) {
case 'LOG':
name += code - 0xa0
break
case 'PUSH':
name += code - 0x5f
break
case 'DUP':
name += code - 0x7f
break
case 'SWAP':
name += code - 0x8f
break
}
return name
}
/**
* Checks if a jump is valid given a destination (defined as a 1 in the validJumps array)
*/
export function jumpIsValid(runState: RunState, dest: number): boolean {
return runState.validJumps[dest] === 1
}
/**
* Returns an overflow-safe slice of an array. It right-pads
* the data with zeros to `length`.
* @param gasLimit requested gas Limit
* @param gasLeft current gas left
* @param runState the current runState
* @param common the common
*/
export function maxCallGas(
gasLimit: bigint,
gasLeft: bigint,
runState: RunState,
common: Common,
): bigint {
if (common.gteHardfork(Hardfork.TangerineWhistle)) {
const gasAllowed = gasLeft - gasLeft / BIGINT_64
return gasLimit > gasAllowed ? gasAllowed : gasLimit
} else {
return gasLimit
}
}
/**
* Subtracts the amount needed for memory usage from `runState.gasLeft`
*/
export function subMemUsage(runState: RunState, offset: bigint, length: bigint, common: Common) {
// YP (225): access with zero length will not extend the memory
if (length === BIGINT_0) return BIGINT_0
const newMemoryWordCount = divCeil(offset + length, BIGINT_32)
if (newMemoryWordCount <= runState.memoryWordCount) return BIGINT_0
const words = newMemoryWordCount
const fee = common.param('memoryGas')
const quadCoefficient = common.param('quadCoefficientDivGas')
// words * 3 + words ^2 / 512
let cost = words * fee + (words * words) / quadCoefficient
if (cost > runState.highestMemCost) {
const currentHighestMemCost = runState.highestMemCost
runState.highestMemCost = cost
cost -= currentHighestMemCost
}
runState.memoryWordCount = newMemoryWordCount
return cost
}
/**
* Writes data returned by evm.call* methods to memory
*/
export function writeCallOutput(runState: RunState, outOffset: bigint, outLength: bigint) {
const returnData = runState.interpreter.getReturnData()
if (returnData.length > 0) {
const memOffset = Number(outOffset)
let dataLength = Number(outLength)
if (BigInt(returnData.length) < dataLength) {
dataLength = returnData.length
}
const data = getDataSlice(returnData, BIGINT_0, BigInt(dataLength))
runState.memory.extend(memOffset, dataLength)
runState.memory.write(memOffset, dataLength, data)
}
// For synthesizer
const returnMemoryPts = runState.interpreter.getReturnMemoryPts()
if (returnMemoryPts.length > 0) {
const acceptMemoryPts = copyMemoryRegion(runState, BIGINT_0, outLength, returnMemoryPts)
for (const entry of acceptMemoryPts) {
// the lower index, the older data
runState.memoryPt.write(
Number(outOffset) + entry.memOffset,
entry.containerSize,
entry.dataPt,
)
}
}
}
/**
* The first rule set of SSTORE rules, which are the rules pre-Constantinople and in Petersburg
*/
export function updateSstoreGas(
runState: RunState,
currentStorage: Uint8Array,
value: Uint8Array,
common: Common,
): bigint {
if (
(value.length === 0 && currentStorage.length === 0) ||
(value.length > 0 && currentStorage.length > 0)
) {
const gas = common.param('sstoreResetGas')
return gas
} else if (value.length === 0 && currentStorage.length > 0) {
const gas = common.param('sstoreResetGas')
runState.interpreter.refundGas(common.param('sstoreRefundGas'), 'updateSstoreGas')
return gas
} else {
/*
The situations checked above are:
-> Value/Slot are both 0
-> Value/Slot are both nonzero
-> Value is zero, but slot is nonzero
Thus, the remaining case is where value is nonzero, but slot is zero, which is this clause
*/
return common.param('sstoreSetGas')
}
}
export function mod(a: bigint, b: bigint) {
let r = a % b
if (r < BIGINT_0) {
r = b + r
}
return r
}
export function fromTwos(a: bigint) {
return BigInt.asIntN(256, a)
}
export function toTwos(a: bigint) {
return BigInt.asUintN(256, a)
}
export function abs(a: bigint) {
if (a > 0) {
return a
}
return a * BIGINT_NEG1
}
const N = BigInt(115792089237316195423570985008687907853269984665640564039457584007913129639936)
export function exponentiation(bas: bigint, exp: bigint) {
let t = BIGINT_1
while (exp > BIGINT_0) {
if (exp % BIGINT_2 !== BIGINT_0) {
t = (t * bas) % N
}
bas = (bas * bas) % N
exp = exp / BIGINT_2
}
return t
}