UNPKG

@tokamak-zk-evm/synthesizer

Version:

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

399 lines 19.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'; export async function finalize(placements, _path, validate, writeToFS = true) { const _validate = validate ?? false; const refactoriedPlacements = refactoryPlacement(placements); let permutation; let placementInstance; 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, prevDataPt, index) => { 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) => { 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) => { 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) { const subcircuitIdByName = new Map(); for (const subcircuitInfo of subcircuitInfos) { subcircuitIdByName.set(subcircuitInfo.name, subcircuitInfo.id); } const dietLoadPlacment = removeUnusedLoadWires(placements); const outPlacements = new Map(); for (const key of placements.keys()) { const placement = key === LOAD_PLACEMENT_INDEX ? dietLoadPlacment : placements.get(key); const newInPts = []; const newOutPts = []; 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, _path, writeToFS = true) { const result = 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 { constructor(placements, instances, _path, writeToFS = true) { this.l = setupParams.l; this.l_D = setupParams.l_D; this._searchInsert = (parent, child) => { 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 = new Map(); groupEntry.set(parentString, true); groupEntry.set(childString, true); this.permGroup.push(groupEntry); }; this._buildPermGroup = () => { 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 = { placementId: placeId, globalWireId: globalWireId, }; const groupEntry = 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 = { 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 = { 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 = 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`) } }; this._validatePermutation = () => { 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 = { 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`); } }; // 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 = { 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); } } _outputPermutation(_path) { 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 = JSON.parse(group[i]); const nextElement = 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".`); } } } const testInstances = async (instances) => { //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 //# sourceMappingURL=finalize.js.map