UNPKG

@tokamak-zk-evm/synthesizer

Version:

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

494 lines (453 loc) 18.2 kB
import { bigIntToBytes, bytesToHex, setLengthLeft } from "@synthesizer-libs/util" import fs from 'fs' import { readFileSync } from 'fs' import path from 'path' import appRootPath from 'app-root-path' import { subcircuits as subcircuitInfos, globalWireList, setupParams, wasmDir } from '../resources/index.js' import { INITIAL_PLACEMENT_INDEX, LOAD_PLACEMENT_INDEX } from '../constant/index.js' // @ts-ignore import { builder } from '../utils/witness_calculator.js' import type { DataPt, PlacementEntry, PlacementInstances, Placements, SubcircuitInfoByName, SubcircuitInfoByNameEntry, } from '../types/index.js' type SubcircuitWireIndex = { subcircuitId: number; localWireId: number } type PlacementWireIndex = { placementId: number; globalWireId: number } export async function finalize( placements: Placements, _path?: string, validate?: boolean, writeToFS: boolean = true ): Promise<{ permutation: Permutation, placementInstance: PlacementInstances }> { const _validate = validate ?? false const refactoriedPlacements = refactoryPlacement(placements) let permutation: Permutation let placementInstance: PlacementInstances if (_validate) { placementInstance = await outputPlacementInstance(refactoriedPlacements, _path, writeToFS) permutation = new Permutation(refactoriedPlacements, placementInstance, _path, writeToFS) return { permutation, placementInstance } } permutation = new Permutation(refactoriedPlacements) return { permutation, placementInstance: [] } } const halveWordSizeOfWires = (newDataPts: DataPt[], prevDataPt: DataPt, index: number): void => { const indLow = BigInt(index * 2) const indHigh = indLow + 1n try { newDataPts[Number(indLow)] = { ...prevDataPt } newDataPts[Number(indHigh)] = { ...prevDataPt } if (prevDataPt.wireIndex !== undefined) { const wireIndex = BigInt(prevDataPt.wireIndex) newDataPts[Number(indLow)].wireIndex = Number(wireIndex * 2n) newDataPts[Number(indHigh)].wireIndex = Number(wireIndex * 2n + 1n) } if (prevDataPt.pairedInputWireIndices !== undefined) { const convertIndices = (ind: number) => { const bigInd = BigInt(ind) return [Number(bigInd * 2n), Number(bigInd * 2n + 1n)] } newDataPts[Number(indHigh)].pairedInputWireIndices = prevDataPt.pairedInputWireIndices.flatMap(convertIndices) newDataPts[Number(indLow)].pairedInputWireIndices = prevDataPt.pairedInputWireIndices.flatMap(convertIndices) } // value가 문자열로 들어올 경우를 대비 const value = typeof prevDataPt.value === 'string' ? BigInt(prevDataPt.value) : prevDataPt.value newDataPts[Number(indHigh)].value = value >> 128n newDataPts[Number(indLow)].value = value & ((2n ** 128n) - 1n) newDataPts[Number(indHigh)].valueHex = bytesToHex( setLengthLeft(bigIntToBytes(newDataPts[Number(indHigh)].value), 16) ) newDataPts[Number(indLow)].valueHex = bytesToHex( setLengthLeft(bigIntToBytes(newDataPts[Number(indLow)].value), 16) ) } catch (error) { console.error('Error in halveWordSizeOfWires:', { error, prevDataPt, index, valueType: typeof prevDataPt.value }); throw error; } } const removeUnusedLoadWires = (placements: Placements): PlacementEntry => { const outLoadPlacement = { ...placements.get(LOAD_PLACEMENT_INDEX)! } const newInPts = [...outLoadPlacement.inPts] const newOutPts = [...outLoadPlacement.outPts] for (let ind = 0; ind < outLoadPlacement.outPts.length; ind++) { let flag = 0 for (const key of placements.keys()) { if (key !== LOAD_PLACEMENT_INDEX) { const placement = placements.get(key)! for (const [_ind, _inPt] of placement.inPts.entries()) { if ( _inPt.source! === LOAD_PLACEMENT_INDEX && _inPt.wireIndex === outLoadPlacement.outPts[ind].wireIndex ) { flag = 1 break } } } if (flag) break } if (!flag) { const arrayIdx = newOutPts.findIndex( (outPt) => outPt.wireIndex! === outLoadPlacement.outPts[ind].wireIndex!, ) newInPts.splice(arrayIdx, 1) newOutPts.splice(arrayIdx, 1) } } outLoadPlacement.inPts = newInPts outLoadPlacement.outPts = newOutPts return outLoadPlacement } function refactoryPlacement(placements: Placements): Placements { const subcircuitIdByName = new Map() for (const subcircuitInfo of subcircuitInfos) { subcircuitIdByName.set(subcircuitInfo.name, subcircuitInfo.id) } const dietLoadPlacment = removeUnusedLoadWires(placements) const outPlacements: Placements = new Map() for (const key of placements.keys()) { const placement = key === LOAD_PLACEMENT_INDEX ? dietLoadPlacment : placements.get(key) const newInPts: DataPt[] = [] const newOutPts: DataPt[] = [] const inPts = placement!.inPts const outPts = placement!.outPts for (const [ind, inPt] of inPts.entries()) { halveWordSizeOfWires(newInPts, inPt, ind) } for (const [ind, outPt] of outPts.entries()) { halveWordSizeOfWires(newOutPts, outPt, ind) } outPlacements.set(key, { name: placement!.name, subcircuitId: subcircuitIdByName.get(placement!.name)!, inPts: newInPts, outPts: newOutPts, }) } return outPlacements } async function outputPlacementInstance( placements: Placements, _path?: string, writeToFS: boolean = true ): Promise<PlacementInstances> { const result: PlacementInstances = Array.from(placements.entries()).map(([key, entry]) => ({ placementIndex: key, subcircuitId: entry.subcircuitId!, instructionName: entry.name, inValues: entry.inPts.map((pt) => pt.valueHex), outValues: entry.outPts.map((pt) => pt.valueHex), })) for (let i = 0; i < INITIAL_PLACEMENT_INDEX; i++) { let ins = result[i].inValues let outs = result[i].outValues const expectedInsLen = subcircuitInfos[result[i].subcircuitId].In_idx[1] const expectedOutsLen = subcircuitInfos[result[i].subcircuitId].Out_idx[1] if (expectedInsLen > ins.length) { const filledIns = ins.concat(Array(expectedInsLen - ins.length).fill('0x00')) result[i].inValues = filledIns } if (expectedOutsLen > outs.length) { const filledOuts = outs.concat(Array(expectedOutsLen - outs.length).fill('0x00')) result[i].outValues = filledOuts } } await testInstances(result) if (writeToFS) { const jsonContent = `${JSON.stringify(result, null, 2)}` const filePath = _path === undefined ? path.resolve( appRootPath.path, 'examples/outputs/placementInstance.json', ) : path.resolve(_path!, 'placementInstance.json') const dir = path.dirname(filePath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } try { fs.writeFileSync(filePath, jsonContent, 'utf-8') console.log( `Synthesizer: Input and output wire assingments of the placements are generated in '${filePath}'.`, ) } catch (error) { throw new Error(`Synthesizer: Failure in writing "placementInstance.json".`) } } return result } // This class instantiates the compiler model in Section "3.1 Compilers" of the Tokamak zk-SNARK paper. class Permutation { private l = setupParams.l private l_D = setupParams.l_D // flattenMapInverse: {0, 1, ..., m_D-1} -> \union_{j=0}^{s_D - 1} {j} \times {0, 1, ...,m^{(j)}-1} } private flattenMapInverse private subcircuitInfoByName: SubcircuitInfoByName private _placements: Placements private _instances: PlacementInstances | undefined private permGroup: Map<string, boolean>[] // permultationY: {0, 1, ..., s_{max}-1} \times {0, 1, ..., l_D-l-1} -> {0, 1, ..., s_{max}-1} public permutationY: number[][] // permutationZ: {0, 1, ..., s_{max}-1} \times {0, 1, ..., l_D-l-1} -> {0, 1, ..., l_D-l-1} public permutationZ: number[][] public permutationFile: { row: number; col: number; Y: number; Z: number }[] constructor( placements: Placements, instances?: PlacementInstances, _path?: string, writeToFS: boolean = true ) { // Istances are needed only for debugging by "this._validatePermutation()" this._placements = placements this._instances = instances ?? undefined this.flattenMapInverse = instances === undefined ? undefined : globalWireList this.subcircuitInfoByName = new Map() for (const subcircuit of subcircuitInfos) { const entryObject: SubcircuitInfoByNameEntry = { id: subcircuit.id, NWires: subcircuit.Nwires, NInWires: subcircuit.In_idx[1], NOutWires: subcircuit.Out_idx[1], inWireIndex: subcircuit.In_idx[0], outWireIndex: subcircuit.Out_idx[0], // wireFlattenMap: \union_{j=0}^{s_D - 1} {j} \times {0, 1, ...,m^{(j)}-1} } -> {0, 1, ..., m_D-1} flattenMap: subcircuit.flattenMap, } this.subcircuitInfoByName.set(subcircuit.name, entryObject) } // Construct permutation this.permGroup = [] this._buildPermGroup() // Equation 8 this.permutationY = Array.from({ length: this._placements.size }, (_, i) => Array.from({ length: this.l_D - this.l }, () => i), ) this.permutationZ = Array.from({ length: this._placements.size }, () => Array.from({ length: this.l_D - this.l }, (_, j) => j), ) this.permutationFile = [] // File write the permutation if (writeToFS) { this._outputPermutation(_path) } } private _outputPermutation(_path?: string) { for (const _group of this.permGroup) { const group = [..._group.keys()] const groupLength = group.length if (groupLength > 1) { for (let i = 0; i < groupLength; i++) { const element: PlacementWireIndex = JSON.parse(group[i]) const nextElement: PlacementWireIndex = JSON.parse(group[(i + 1) % groupLength]) this.permutationFile.push({ row: element.placementId, col: element.globalWireId - this.l, Y: nextElement.placementId, Z: nextElement.globalWireId - this.l, }) const rowIdx = this.permutationFile[this.permutationFile.length - 1].row const colIdx = this.permutationFile[this.permutationFile.length - 1].col if (this.permutationY[rowIdx] === undefined) { console.log(`debug`) } this.permutationY[rowIdx][colIdx] = this.permutationFile[this.permutationFile.length - 1].Y this.permutationZ[rowIdx][colIdx] = this.permutationFile[this.permutationFile.length - 1].Z } } } if (this._instances !== undefined) { this._validatePermutation() } const jsonContent = `${JSON.stringify(this.permutationFile, null, 2)}` const filePath = _path === undefined ? path.resolve( appRootPath.path, 'examples/outputs/permutation.json', ) : path.resolve(_path!, 'permutation.json') const dir = path.dirname(filePath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } try { fs.writeFileSync(filePath, jsonContent, 'utf-8') console.log(`Synthesizer: Permutation rule is generated in '${filePath}'.`) } catch (error) { throw new Error(`Synthesizer: Failure in writing "permutation.json".`) } } private _searchInsert = (parent: PlacementWireIndex, child: PlacementWireIndex): void => { const parentString = JSON.stringify({ ...parent }) const childString = JSON.stringify({ ...child }) for (const group of this.permGroup) { if (group.has(parentString)) { group.set(childString, true) return } } const groupEntry: Map<string, boolean> = new Map() groupEntry.set(parentString, true) groupEntry.set(childString, true) this.permGroup.push(groupEntry) } private _buildPermGroup = (): void => { for (const placeId of this._placements.keys()) { const thisPlacement = this._placements.get(placeId)! const thisSubcircuitInfo = this.subcircuitInfoByName.get(thisPlacement.name)! for (let i = 0; i < thisSubcircuitInfo.NOutWires; i++) { const localWireId = thisSubcircuitInfo.outWireIndex + i const globalWireId = thisSubcircuitInfo.flattenMap![localWireId] if (!(globalWireId >= this.l && globalWireId < this.l_D)) { break } const placementWireId: PlacementWireIndex = { placementId: placeId, globalWireId: globalWireId, } const groupEntry: Map<string, boolean> = new Map() groupEntry.set(JSON.stringify({ ...placementWireId }), true) this.permGroup.push(groupEntry) } } for (const placeId of this._placements.keys()) { const thisPlacement = this._placements.get(placeId)! const thisSubcircuitInfo = this.subcircuitInfoByName.get(thisPlacement.name)! for (let i = 0; i < thisSubcircuitInfo.NInWires; i++) { const localWireId = thisSubcircuitInfo.inWireIndex + i const globalWireId = thisSubcircuitInfo.flattenMap![localWireId] if (!(globalWireId >= this.l && globalWireId < this.l_D)) { break } const placementWireId: PlacementWireIndex = { placementId: placeId, globalWireId: globalWireId, } const dataPt = thisPlacement.inPts[i] let hasParent = false if (dataPt !== undefined) { if (typeof dataPt.source === 'number') { if (dataPt.source !== placeId) { hasParent = true const pointedSubcircuitInfo = this.subcircuitInfoByName.get( this._placements.get(dataPt.source!)!.name, )! const pointedWireId = this._placements .get(dataPt.source!)! .outPts.findIndex((outPt) => outPt.wireIndex! === dataPt.wireIndex!) if (pointedWireId === -1){ throw new Error(`Permutation: A wire is referring to nothing.`) } const pointedLocalWireId = pointedSubcircuitInfo.outWireIndex + pointedWireId const pointedGlobalWireId = pointedSubcircuitInfo.flattenMap![pointedLocalWireId] const pointedPlacementWireId: PlacementWireIndex = { placementId: dataPt.source, globalWireId: pointedGlobalWireId, } if (!(pointedGlobalWireId >= this.l && pointedGlobalWireId < this.l_D)) { throw new Error(`Permutation: A wire is referring to a public wire or an internal wire.`) } this._searchInsert(pointedPlacementWireId, placementWireId) } } } if (!hasParent) { const groupEntry: Map<string, boolean> = new Map() groupEntry.set(JSON.stringify({ ...placementWireId }), true) this.permGroup.push(groupEntry) } } // console.log(`Length inc: ${thisSubcircuitInfo.NInWires}`) // let checksum = 0 // for (const group of this.permGroup){ // checksum += group.size // } // console.log(`checksum: ${checksum}`) // console.log(`a`) } } private _validatePermutation = (): void => { let permutationDetected = false for (const [placementId, instance] of this._instances!.entries()) { const rawInstance = [1, ...instance.outValues, ...instance.inValues] const thisSubcircuitInfo = this.subcircuitInfoByName.get(instance.instructionName)! const thisSubcircuitId = thisSubcircuitInfo.id for (let idx = 1; idx < rawInstance.length; idx++) { const thisLocalWireId = idx const inversedKey: SubcircuitWireIndex = { subcircuitId: thisSubcircuitId, localWireId: idx, } const thisGlobalWireId = thisSubcircuitInfo.flattenMap![thisLocalWireId] if (thisGlobalWireId < this.l) { break } const nextPlacementId = this.permutationY[placementId][thisGlobalWireId - this.l] const nextGlobalWireId = this.permutationZ[placementId][thisGlobalWireId - this.l] + this.l const nextLocalWireId = this.flattenMapInverse![nextGlobalWireId][1] const nextRawInstance = [ 1, ...this._instances![nextPlacementId].outValues, ...this._instances![nextPlacementId].inValues, ] if (thisLocalWireId !== nextLocalWireId) { permutationDetected = true if (rawInstance[thisLocalWireId] !== nextRawInstance[nextLocalWireId]) { throw new Error(`Permutation: Permutation does not hold.`) } } } } if (permutationDetected === false) { console.log(`Synthesizer: Warning: No permutation detected!`) } else { console.log(`Synthesizer: Permutation check clear`) } } } const testInstances = async (instances: PlacementInstances): Promise<void> => { //console.log("Usage: tsx generate_witness.ts <file.wasm> <input.json> <output.wtns>") const reuseBuffer = new Map() for (const [placementInd, instance] of instances.entries()) { const id = instance.subcircuitId let buffer if (reuseBuffer.has(id)) { buffer = reuseBuffer.get(id) } else { const targetWasmPath = path.resolve(appRootPath.path, wasmDir, `subcircuit${id}.wasm`) try { buffer = readFileSync(targetWasmPath) } catch (err) { throw new Error(`Error while reading subcircuit${id}.wasm`) } reuseBuffer.set(id, buffer) } const ins = { in: instance.inValues } const witnessCalculator = await builder(buffer) const witness = await witnessCalculator.calculateWitness(ins, 0) for (let i = 1; i <= instance.outValues.length; i++) { if (witness[i] !== BigInt(instance.outValues[i - 1])) { throw new Error( `Instance check failed in the placement ${instance.instructionName} (index = ${placementInd})`, ) } } } console.log(`Synthesizer: Instances passed subcircuits.`) } // Todo: Compresss permutation