UNPKG

@tokamak-zk-evm/synthesizer

Version:

Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation

216 lines (188 loc) 6.39 kB
import { ERROR, EvmError } from '../../exceptions.js' import type { DataPt } from '../types/index.js' /** * Key differences between Stack and StackPt classes * * 1. Data Type * - Stack: bigint[] (stores actual values) * - StackPt: DataPt[] (stores data pointers) * * 2. Purpose * - Stack: Used in actual EVM execution * - StackPt: Used for symbolic execution * * 3. Operation Handling * - Stack: Performs operations on actual values (e.g., actual addition) * - StackPt: Manages pointers for data flow tracking * * 4. Usage * - Stack: Actual transaction processing, contract execution * - StackPt: Program analysis, optimization, bug detection * * Both classes provide the same interface (push, pop, swap, etc.), * but operate internally for different purposes. */ export type TStackPt = DataPt[] /** * Stack implementation for EVM symbolic execution * * Key Features: * 1. Purpose * - Stack used in EVM symbolic execution * - Manages DataPt type data pointers * * 2. Memory Management * - Once allocated, array size never decreases * - During pop operations, _len is decreased instead of actually deleting items * - This is an optimization strategy to reduce memory reallocation costs * * 3. Key Constraints * - Maximum stack height (_maxHeight) limit * - Stack overflow/underflow checks * * 4. Main Operations * - push: Add new data pointer * - pop: Remove top data pointer * - peek: View stack contents * - swap: Exchange positions of stack items * - dup: Duplicate stack items * * This class provides the same interface as the actual EVM stack, * but is characterized by handling data pointers (DataPt) instead of actual values (bigint). */ export class StackPt { // This array is initialized as an empty array. Once values are pushed, the array size will never decrease. // Internal array storing actual data. Size doesn't decrease after push private _storePt: TStackPt // Maximum allowed stack height (default: 1024) private _maxHeight: number // Actual number of items currently in use in the stack private _len: number = 0 constructor(maxHeight?: number) { // It is possible to initialize the array with `maxHeight` items. However, // this makes the constructor 10x slower and there do not seem to be any observable performance gains this._storePt = [] this._maxHeight = maxHeight ?? 1024 } get length() { return this._len } push(pt: DataPt) { if (this._len >= this._maxHeight) { throw new EvmError(ERROR.STACK_OVERFLOW) } if (typeof pt.source !== 'number') { throw new Error( `Synthesizer: Push to StackPt: Every item on the StackPt must indicate an output wire of a subcirciut placement.`, ) } // Read current length, set `_storePt` to value, and then increase the length this._storePt[this._len++] = pt } pop(): DataPt { if (this._len < 1) { throw new EvmError(ERROR.STACK_UNDERFLOW) } // Length is checked above, so pop shouldn't return undefined // First decrease current length, then read the item and return it // Note: this does thus not delete the item from the internal array // However, the length is decreased, so it is not accessible to external observers return this._storePt[--this._len] } /** * Pop multiple items from stack. Top of stack is first item * in returned array. * @param num - Number of items to pop */ popN(num: number = 1): DataPt[] { if (this._len < num) { throw new EvmError(ERROR.STACK_UNDERFLOW) } if (num === 0) { return [] } const arr = Array(num) const cache = this._storePt for (let pop = 0; pop < num; pop++) { // Note: this thus also (correctly) reduces the length of the internal array (without deleting items) arr[pop] = cache[--this._len] } return arr } /** * Return items from the stack * @param num Number of items to return * @throws {@link ERROR.STACK_UNDERFLOW} */ peek(num: number = 1): DataPt[] { const peekArray: DataPt[] = Array(num) let start = this._len for (let peek = 0; peek < num; peek++) { const index = --start if (index < 0) { throw new EvmError(ERROR.STACK_UNDERFLOW) } peekArray[peek] = this._storePt[index] } return peekArray } /** * Swap top of stack with an item in the stack. * @param position - Index of item from top of the stack (0-indexed) */ swap(position: number) { if (this._len <= position) { throw new EvmError(ERROR.STACK_UNDERFLOW) } const head = this._len - 1 const i = head - position const storageCached = this._storePt const tmp = storageCached[head] storageCached[head] = storageCached[i] storageCached[i] = tmp } /** * Pushes a copy of an item in the stack. * @param position - Index of item to be copied (1-indexed) */ // I would say that we do not need this method any more // since you can't copy a primitive data type // Nevertheless not sure if we "loose" something here? // Will keep commented out for now dup(position: number) { const len = this._len if (len < position) { throw new EvmError(ERROR.STACK_UNDERFLOW) } // Note: this code is borrowed from `push()` (avoids a call) if (len >= this._maxHeight) { throw new EvmError(ERROR.STACK_OVERFLOW) } const i = len - position this._storePt[this._len++] = this._storePt[i] } /** * Swap number 1 with number 2 on the stack * @param swap1 * @param swap2 */ exchange(swap1: number, swap2: number) { const headIndex = this._len - 1 const exchangeIndex1 = headIndex - swap1 const exchangeIndex2 = headIndex - swap2 // Stack underflow is not possible in EOF if (exchangeIndex1 < 0 || exchangeIndex2 < 0) { throw new EvmError(ERROR.STACK_UNDERFLOW) } const cache = this._storePt[exchangeIndex2] this._storePt[exchangeIndex2] = this._storePt[exchangeIndex1] this._storePt[exchangeIndex1] = cache } /** * Returns a copy of the current stack. This represents the actual state of the stack * (not the internal state of the stack, which might have unreachable elements in it) */ getStack() { return this._storePt.slice(0, this._len) } }