@tokamak-zk-evm/synthesizer
Version:
Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation
437 lines (398 loc) • 15.8 kB
text/typescript
import {
BIGINT_0,
bigIntToBytes,
// bytesToHex,
setLengthLeft,
// setLengthRight,
} from "@synthesizer-libs/util"
import { Memory } from '../../memory.js'
import type { RunState } from '../../interpreter.js'
import type { DataPt } from '../types/index.js'
/**
* Key differences between Memory and MemoryPt classes
*
* 1. Data Structure
* - Memory: Uint8Array (continuous byte array)
* - MemoryPt: Map<number, { memOffset, containerSize, dataPt }> (memory pointer map)
*
* 2. Storage Method
* - Memory: Directly stores actual byte values in continuous memory
* - MemoryPt: Manages data location and size information through pointers
*
* 3. Read/Write Operations
* - Memory: Direct read/write to actual memory
* - MemoryPt:
* - Write: Creates new data pointers and manages overlapping regions
* - Read: Returns data alias information through getDataAlias
*
* 4. Purpose
* - Memory: Memory manipulation during actual EVM execution
* - MemoryPt: Memory tracking and analysis for symbolic execution
*
* 5. Characteristics
* - Memory: Continuous memory space, simple byte manipulation
* - MemoryPt:
* - Timestamp-based data management
* - Memory region conflict detection
* - Data alias information generation
*/
/**
* Structure representing data alias information.
* @property {DataPt} dataPt - Original data pointer
* @property {number} shift - Number of bit shifts (positive for SHL, negative for SHR)
* @property {string} masker - Hexadecimal string representing valid bytes (FF) or invalid bytes (00)
*/
export type DataAliasInfoEntry = { dataPt: DataPt; shift: number; masker: string }
export type DataAliasInfos = DataAliasInfoEntry[]
/**
* Structure representing memory information.
* @property {number} memOffset - Memory offset
* @property {number} containerSize - Container size
* @property {DataPt} dataPt - Data pointer
*/
export type MemoryPtEntry = { memOffset: number; containerSize: number; dataPt: DataPt }
/**
* Array of memory information. Lower indices represent older memory information.
*/
export type MemoryPts = MemoryPtEntry[]
/**
* Map of memory information.
*/
type TMemoryPt = Map<number, MemoryPtEntry>
/**
* Map representing data fragment information.
* @property {number} key - Timestamp when data was stored in memory
* @property {Set<number>} originalRange - Original data range
* @property {Set<number>} validRange - Valid data range
*/
type _DataFragments = Map<number, { originalRange: Set<number>; validRange: Set<number> }>
/**
* Creates a set of consecutive numbers from a to b.
* Commonly used for:
* - Representing memory address ranges occupied by specific data (e.g., data from offset 2 to 5)
* - Tracking valid memory ranges (e.g., valid memory regions before and after overwriting)
* @param a - Start number
* @param b - End number
* @returns Set containing consecutive numbers from a to b
*/
const createRangeSet = (a: number, b: number): Set<number> => {
// the resulting increasing set from 'a' to 'b'
return new Set(Array.from({ length: b - a + 1 }, (_, i) => a + i))
}
/**
* A minus B
* @param A - First set
* @param B - Second set
* @returns A minus B
*/
const setMinus = (A: Set<number>, B: Set<number>): Set<number> => {
const result = new Set<number>()
for (const element of A) {
if (!B.has(element)) {
result.add(element)
}
}
return result
}
export const simulateMemoryPt = (memoryPts: MemoryPts): MemoryPt => {
const simMemPt = new MemoryPt()
for (let k = 0; k < memoryPts.length; k++) {
// the lower index, the older data
simMemPt.write(memoryPts[k].memOffset, memoryPts[k].containerSize, memoryPts[k].dataPt)
}
return simMemPt
}
export const copyMemoryRegion = (
runState: RunState,
srcOffset: bigint,
length: bigint,
fromMemoryPts?: MemoryPts,
dstOffset?: bigint,
): MemoryPts => {
const srcOffsetNum = Number(srcOffset)
const dstOffsetNum = Number(dstOffset ?? 0)
const lengthNum = Number(length)
let toMemoryPts: MemoryPts
if (fromMemoryPts === undefined) {
toMemoryPts = runState.memoryPt.read(srcOffsetNum, lengthNum)
} else {
const simFromMemoryPt = simulateMemoryPt(fromMemoryPts)
toMemoryPts = simFromMemoryPt.read(srcOffsetNum, lengthNum)
}
const zeroMemoryPtEntry: MemoryPtEntry = {
memOffset: dstOffsetNum,
containerSize: lengthNum,
dataPt: runState.synthesizer.loadAuxin(BIGINT_0),
}
if (toMemoryPts.length > 0) {
const simToMemoryPt = simulateMemoryPt(toMemoryPts)
const dataAliasInfos = simToMemoryPt.getDataAlias(srcOffsetNum, lengthNum)
if (dataAliasInfos.length > 0) {
const resolvedDataPts = runState.synthesizer.placeMemoryToMemory(dataAliasInfos)
runState.synthesizer.adjustMemoryPts(
resolvedDataPts,
toMemoryPts,
srcOffsetNum,
dstOffsetNum,
lengthNum,
)
} else {
toMemoryPts.push(zeroMemoryPtEntry)
}
} else {
toMemoryPts.push(zeroMemoryPtEntry)
}
return toMemoryPts
}
/*eslint-disable */
const CONTAINER_SIZE = 8192
/**
* Memory implements a simple memory model
* for the ethereum virtual machine.
*/
export class MemoryPt {
_storePt: TMemoryPt
private _timeStamp: number
constructor() {
this._storePt = new Map()
this._timeStamp = 0
}
/**
* Cleans up memory pointers when new data is written.
* If the newly written data completely overlaps existing data,
* delete the existing key-value pair.
*/
private _memPtCleanUp(newOffset: number, newSize: number) {
for (const [key, { memOffset: _offset, containerSize: _size }] of this._storePt) {
// Condition where new data completely overlaps existing data
const _endOffset = _offset + _size - 1
const newEndOffset = newOffset + newSize - 1
if (_endOffset <= newEndOffset && _offset >= newOffset) {
this._storePt.delete(key)
}
}
}
/**
* Writes a byte array with length `size` to memory, starting from `offset`.
* @param offset - Starting memory position
* @param containerSize - How many bytes to write
* @param dataPt - Data pointer
*/
write(offset: number, size: number, dataPt: DataPt) {
if (size === 0) {
return
}
// if setLengthLeft(bigIntToBytes(dataPt.value), 32).length !== size) throw new Error('Invalid value size')
// if (offset + size > this._storePt.length) throw new Error('Value exceeds memory capacity')
this._memPtCleanUp(offset, size)
this._storePt.set(this._timeStamp++, {
memOffset: offset,
containerSize: size,
dataPt,
})
}
/**
* Returns values of _storePt elements (excluding keys) that affect a specific memory range. Used when moving data from Memory to Memory.
* @param offset - Starting memory position to read
* @param length - Number of bytes to read
* @returns {returnMemroyPts}
*/
read(offset: number, length: number, avoidCopy?: boolean): MemoryPts {
const dataFragments = this._viewMemoryConflict(offset, length)
const returnMemoryPts: MemoryPts = []
if (dataFragments.size > 0) {
const sortedKeys = Array.from(dataFragments.keys()).sort((a, b) => a - b)
sortedKeys.forEach((key) => {
if (avoidCopy === true) {
returnMemoryPts.push(this._storePt.get(key)!)
} else {
const target = this._storePt.get(key)!
const copy: MemoryPtEntry = {
memOffset: target.memOffset,
containerSize: target.containerSize,
dataPt: target.dataPt,
}
returnMemoryPts.push(copy)
}
})
}
return returnMemoryPts
}
/**
* read is not used for MemoryPt manipulation. Instead, "getDataAlias" is used.
* Reads a slice of memory from `offset` till `offset + size` as a `Uint8Array`.
* It fills up the difference between memory's length and `offset + size` with zeros.
* @param offset - Starting memory position
* @param size - How many bytes to read
* @param avoidCopy - Avoid memory copy if possible for performance reasons (optional)
read(offset: number, size: number): Uint8Array {
const loaded = this._storePt.subarray(offset, offset + size)
if (avoidCopy === true) {
return loaded
}
const returnBytes = new Uint8Array(size)
// Copy the stored "buffer" from memory into the return Uint8Array
returnBytes.set(loaded)
return returnBytes
}
*/
/**
* Returns data transformation information for a specific memory range. Used when moving data from Memory to Stack.
* @param offset - Starting memory position to read
* @param size - Number of bytes to read
* @returns {DataAliasInfos}
*/
getDataAlias(offset: number, size: number): DataAliasInfos {
const dataAliasInfos: DataAliasInfos = []
const dataFragments = this._viewMemoryConflict(offset, size)
const sortedTimeStamps = Array.from(dataFragments.keys()).sort((a, b) => a - b)
for (const timeStamp of sortedTimeStamps) {
const _value = dataFragments.get(timeStamp)!
const dataEndOffset =
this._storePt.get(timeStamp)!.memOffset + this._storePt.get(timeStamp)!.containerSize - 1
const viewEndOffset = offset + size - 1
dataAliasInfos.push({
dataPt: this._storePt.get(timeStamp)!.dataPt,
// shift is positive for SHL, negative for SHR
shift: (viewEndOffset - dataEndOffset) * 8,
masker: this._generateMasker(offset, size, _value.validRange),
})
}
return dataAliasInfos
}
viewMemory(offset: number, length: number): Uint8Array {
const BIAS = 0x100000 // Any large number
const memoryPts = this.read(offset, length)
const simMem = new Memory()
for (const memoryPtEntry of memoryPts) {
const containerOffset = memoryPtEntry.memOffset
const containerSize = memoryPtEntry.containerSize
const buf = setLengthLeft(bigIntToBytes(memoryPtEntry.dataPt.value), containerSize)
simMem.write(containerOffset + BIAS, containerSize, buf)
// // Find the offset where nonzero value starts
// const storedOffset = storedEndOffset - this._storePt.get(timeStamp)!.dataPt.sourceSize + 1
// // If data is in the range
// if (storedEndOffset >= offset && storedOffset <= endOffset) {
// const _offset = this._storePt.get(timeStamp)!.memOffset // This data offset can be negative.
// const _containerSize = this._storePt.get(timeStamp)!.containerSize
// const _actualSize = this._storePt.get(timeStamp)!.dataPt.sourceSize
// const value = this._storePt.get(timeStamp)!.dataPt.value
// let valuePadded = setLengthLeft(bigIntToBytes(value), _actualSize)
// if (_containerSize < _actualSize){
// valuePadded = valuePadded.slice(0, _containerSize)
// }
// console.log(bytesToHex(valuePadded))
// simMem.write(_offset + BIAS, Math.min(_containerSize, _actualSize), valuePadded)
// }
}
return simMem.read(offset + BIAS, length)
}
/**
* Finds conflicting data fragments in the memory region.
* @param offset - Starting memory position to read
* @param size - Number of bytes to read
* @returns {DataFragments}
*/
private _viewMemoryConflict(offset: number, size: number): _DataFragments {
const dataFragments: _DataFragments = new Map()
const endOffset = offset + size - 1
const sortedTimeStamps = Array.from(this._storePt.keys()).sort((a, b) => a - b)
let i = 0
for (const timeStamp of sortedTimeStamps) {
const containerOffset = this._storePt.get(timeStamp)!.memOffset
const containerEndOffset = containerOffset + this._storePt.get(timeStamp)!.containerSize - 1
// Find the offset where nonzero value starts
const sortedTimeStamps_firsts = sortedTimeStamps.slice(0, i)
// If data is in the range
if (containerEndOffset >= offset && containerOffset <= endOffset) {
const overlapStart = Math.max(offset, containerOffset)
const overlapEnd = Math.min(endOffset, containerEndOffset)
const thisDataOriginalRange = createRangeSet(containerOffset, containerEndOffset)
const thisDataValidRange = createRangeSet(overlapStart, overlapEnd)
dataFragments.set(timeStamp, {
originalRange: thisDataOriginalRange,
validRange: thisDataValidRange,
})
// Update previous data overlap ranges
for (const _timeStamp of sortedTimeStamps_firsts) {
if (dataFragments.has(_timeStamp)) {
const overwrittenRange = setMinus(
dataFragments.get(_timeStamp)!.validRange,
dataFragments.get(timeStamp)!.validRange,
)
if (overwrittenRange.size <= 0) {
dataFragments.delete(_timeStamp)
} else {
dataFragments.set(_timeStamp, {
originalRange: dataFragments.get(_timeStamp)!.originalRange,
validRange: overwrittenRange,
})
}
}
}
}
i++
}
return dataFragments
}
// private _viewMemoryConflict(offset: number, size: number): _DataFragments {
// const dataFragments: _DataFragments = new Map()
// const endOffset = offset + size - 1
// const sortedTimeStamps = Array.from(this._storePt.keys()).sort((a, b) => a - b)
// let i = 0
// for (const timeStamp of sortedTimeStamps) {
// const containerOffset = this._storePt.get(timeStamp)!.memOffset
// const storedEndOffset = containerOffset + this._storePt.get(timeStamp)!.containerSize - 1
// // Find the offset where nonzero value starts
// const storedOffset = storedEndOffset - this._storePt.get(timeStamp)!.dataPt.sourceSize + 1
// const sortedTimeStamps_firsts = sortedTimeStamps.slice(0, i)
// // If data is in the range
// if (storedEndOffset >= offset && storedOffset <= endOffset) {
// const overlapStart = Math.max(offset, storedOffset)
// const overlapEnd = Math.min(endOffset, storedEndOffset)
// const thisDataOriginalRange = createRangeSet(storedOffset, storedEndOffset)
// const thisDataValidRange = createRangeSet(overlapStart, overlapEnd)
// dataFragments.set(timeStamp, {
// originalRange: thisDataOriginalRange,
// validRange: thisDataValidRange,
// })
// // Update previous data overlap ranges
// for (const _timeStamp of sortedTimeStamps_firsts) {
// if (dataFragments.has(_timeStamp)) {
// const overwrittenRange = setMinus(
// dataFragments.get(_timeStamp)!.validRange,
// dataFragments.get(timeStamp)!.validRange,
// )
// if (overwrittenRange.size <= 0) {
// dataFragments.delete(_timeStamp)
// } else {
// dataFragments.set(_timeStamp, {
// originalRange: dataFragments.get(_timeStamp)!.originalRange,
// validRange: overwrittenRange,
// })
// }
// }
// }
// }
// i++
// }
// return dataFragments
// }
private _generateMasker(offset: number, size: number, validRange: Set<number>): string {
const targetRange = createRangeSet(offset, offset + size - 1)
for (const element of validRange) {
if (!targetRange.has(element)) {
throw new Error('Error: arg2 is not a subset of arg1')
}
}
let maskerString = '0x'
for (const element of targetRange) {
if (validRange.has(element)) {
maskerString += 'FF'
} else {
maskerString += '00'
}
}
return maskerString
}
}