UNPKG

@nori-zk/o1js-zk-utils

Version:

o1js-zk-utils supporting Nori Bridge

309 lines 12.8 kB
import { Bool, Field, Provable, Poseidon, UInt64, } from 'o1js'; import { wordToBytes } from '@nori-zk/proof-conversion/min'; import { Bytes20, Bytes32 } from './types.js'; // Bytes32 Field utils export function isLessThanFieldPrimeLE(bytes) { // Mina field prime p in LE as Fields const P_LE = [ 1n, 0n, 0n, 0n, 237n, 48n, 45n, 153n, 27n, 249n, 76n, 9n, 252n, 152n, 70n, 34n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 64n, ].map((b) => new Field(b)); // Scan from bytes[31] (MSB) down to bytes[0] (LSB). // `strictlyLess` starts false and can only ever flip true — never reset. // `different` latches true the moment any byte differs from p's byte. // Once `different` is true, `different.not()` is false, so no future byte // can update `strictlyLess` — it is frozen at whatever it was set to. // Therefore: `strictlyLess` becomes true if at the first differing byte // position (from MSB), the input byte was less than p's byte. // If all bytes match (input == p), `different` never latches and // `strictlyLess` stays false — correctly rejecting p itself. let strictlyLess = Bool(false); let different = Bool(false); for (let i = 31; i >= 0; i--) { const pField = P_LE[i]; const byteField = bytes[i].value; const lt = byteField.lessThan(pField); const eq = byteField.equals(pField); strictlyLess = Provable.if(different.not().and(lt), Bool, Bool(true), strictlyLess); different = different.or(eq.not()); } return strictlyLess; } export function bytes32LEToFieldProvable(uint8ArrayLength32) { // What if the Bytes32 represents a value greater than P - 1? We should do some // assertion that it isnt CHECKME // See 'Field order wrapping bytes 32 validation' /*isLessThanFieldPrimeLE(uint8ArrayLength32).assertTrue( 'Given a uint8ArrayLength32 which exceeded p - 1' );*/ // CHECKME removing this for now.... I think it may be redundant as the FixedBytes are constructed // By the first zkprogram and it is always within the range of p - 1 by definition. // On the rust side we have fixed_bytes[..32].copy_from_slice(&root.to_bytes()); // root.to_bytes() is in LE so reconstruct it as such. let field = new Field(0); for (let i = 31; i >= 0; i--) { field = field.mul(256).add(uint8ArrayLength32[i].value); } return field; } export function uint8ArrayToBigIntBE(bytes) { return bytes.reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n); } export function uint8ArrayToBigIntLE(bytes) { return bytes.reduceRight((acc, byte) => (acc << 8n) + BigInt(byte), 0n); } export function fieldToHexBE(field) { const bytesLE = wordToBytes(field, 32); // This is LE const bytesBE = bytesLE.reverse(); return `0x${bytesBE .map((byte) => byte.toBigInt().toString(16).padStart(2, '0')) .join('')}`; } export function fieldToBigIntBE(field) { const bytesLE = wordToBytes(field, 32); // This is LE const bytesBE = bytesLE.reverse(); return bytesBE.reduce((acc, byte) => (acc << 8n) + byte.toBigInt(), 0n); } export function fieldToHexLE(field) { const bytesLE = wordToBytes(field, 32); // This is LE return `0x${bytesLE .map((byte) => byte.toBigInt().toString(16).padStart(2, '0')) .join('')}`; } export function fieldToBigIntLE(field) { const bytesLE = wordToBytes(field, 32); // This is LE return bytesLE.reduce((acc, byte) => (acc << 8n) + byte.toBigInt(), 0n); } // This is explicitly here for validation puposes not supposed to be provable. function toBigIntFromBytes(bytes) { let result = 0n; for (const byte of bytes) { result = (result << 8n) | BigInt(byte); } return result; } // This is explicitly here for validation puposes not supposed to be provable. const MAX_U64 = (1n << 64n) - 1n; function assertUint64(value) { if (value < 0n || value > MAX_U64) { throw new RangeError(`Value out of range for u64: '${value}'.`); } } // Proof decoder const proofOffsets = { inputSlot: 0, inputStoreHash: 8, outputSlot: 40, outputStoreHash: 48, executionStateRoot: 80, verifiedContractStorageSlotsRoot: 112, nextSyncCommitteeHash: 144, contractAddress: 176, genesisRoot: 196, }; const proofTotalLength = 228; export function decodeConsensusMptProof(ethSP1Proof) { const proofData = new Uint8Array(ethSP1Proof.public_values.buffer.data // Buffer.from() this is nodejs specific and seemingly redundant ); if (proofData.length !== proofTotalLength) { throw new Error(`Byte slice must be exactly ${proofTotalLength} bytes, got '${proofData.length}'.`); } const inputSlotSlice = proofData.slice(proofOffsets.inputSlot, proofOffsets.inputStoreHash); const inputSlot = toBigIntFromBytes(inputSlotSlice); assertUint64(inputSlot); const inputStoreHashSlice = proofData.slice(proofOffsets.inputStoreHash, proofOffsets.outputSlot); const outputSlotSlice = proofData.slice(proofOffsets.outputSlot, proofOffsets.outputStoreHash); const outputSlot = toBigIntFromBytes(outputSlotSlice); assertUint64(outputSlot); const outputStoreHashSlice = proofData.slice(proofOffsets.outputStoreHash, proofOffsets.executionStateRoot); const executionStateRootSlice = proofData.slice(proofOffsets.executionStateRoot, proofOffsets.verifiedContractStorageSlotsRoot); const verifiedContractStorageSlotsRootSlice = proofData.slice(proofOffsets.verifiedContractStorageSlotsRoot, proofOffsets.nextSyncCommitteeHash); const nextSyncCommitteeHashSlice = proofData.slice(proofOffsets.nextSyncCommitteeHash, proofOffsets.contractAddress); const contractAddressSlice = proofData.slice(proofOffsets.contractAddress, proofOffsets.genesisRoot); const genesisRootSlice = proofData.slice(proofOffsets.genesisRoot, proofTotalLength); const provables = { inputSlot: UInt64.from(inputSlot), inputStoreHash: Bytes32.from(inputStoreHashSlice), outputSlot: UInt64.from(outputSlot), outputStoreHash: Bytes32.from(outputStoreHashSlice), executionStateRoot: Bytes32.from(executionStateRootSlice), verifiedContractDepositsRoot: Bytes32.from(verifiedContractStorageSlotsRootSlice), nextSyncCommitteeHash: Bytes32.from(nextSyncCommitteeHashSlice), contractAddress: Bytes20.from(contractAddressSlice), genesisRoot: Bytes32.from(genesisRootSlice), }; return provables; } export function extractEthTokenBridgeAddressFromSP1Proof(example) { const decoded = decodeConsensusMptProof(example.sp1PlonkProof); return new Bytes20(decoded.contractAddress.bytes).toField(); } export function extractGenesisRootFromSP1Proof(example) { const decoded = decodeConsensusMptProof(example.sp1PlonkProof); return Poseidon.hash(new Bytes32(decoded.genesisRoot.bytes).toFields()); } // Compile and verify contracts utility export async function compileAndVerifyContracts(logger, contracts) { try { const results = {}; const mismatches = []; for (const { name, program, integrityHash } of contracts) { logger.log(`Compiling ${name} contract.`); const timer = createTimer(); const compiled = await program.compile(); logger.log(`${name} compiled in ${timer()}`); const verificationKey = compiled.verificationKey; const calculatedHash = verificationKey.hash.toString(); logger.log(`${name} contract vk hash compiled: '${calculatedHash}'`); results[`${name}VerificationKey`] = verificationKey; if (calculatedHash !== integrityHash) { mismatches.push(`${name}: Computed hash '${calculatedHash}' ` + `doesn't match expected hash '${integrityHash}'`); } } if (mismatches.length > 0) { const errorMessage = [ 'Verification key hash mismatch detected:', ...mismatches, '', `Refusing to start. Try clearing your o1js cache directory, typically found at '~/.cache/o1js'. Or do you need to run 'npm run bake-vk-hashes' in the eth-processor or o1js-zk-utils nori-bridge-sdk folder and commit the change?`, ].join('\n'); throw new Error(errorMessage); } logger.log('All contracts compiled and verified successfully.'); return results; } catch (err) { logger.error(`Error compiling contracts:\n${String(err)}`); logger.error(err.stack); throw err; } } export function vkToVkSafe(vk) { const { data, hash } = vk; return { hashStr: hash.toBigInt().toString(), data, }; } export function vkSafeToVk(vkSafe) { return { data: vkSafe.data, hash: new Field(BigInt(vkSafe.hashStr)), }; } /** * Compiles a list of SmartContracts or CompilableZkPrograms and optionally verifies their * verification key hashes against provided integrity hashes. * * @template T - An array of contract descriptors. Each descriptor must include: * - `name`: The contract/program name (used as a key for the returned verification key). * - `program`: Either a `SmartContract` class or a `CompilableZkProgram`. * - `integrityHash` (optional): The expected verification key hash to validate against. * * @param logger - Logger object with a `.log(string)` method for outputting progress messages. * Type: `{ log: (msg: string) => void }`. * @param contracts - Array of contract/program descriptors to compile and optionally verify. * @param cacheConfig - Optional cache configuration (`FileSystem` or `Network`) to use during compilation. * * @returns A Promise resolving to an object mapping each contract name to its `VerificationKey`. * Keys are of the form `${name}VerificationKey`. * * @throws Will throw an Error if any computed verification key hash does not match * its expected `integrityHash`, including a helpful message on clearing the cache * or regenerating verification keys. * * Example usage: * ```ts * const vks = await compileAndOptionallyVerifyContracts( * { log: console.log }, * [ * { name: 'MyContract', program: MyContract, integrityHash: '12345' }, * { name: 'MyProgram', program: MyZkProgram }, * ], * cacheConfig * ); * ``` */ export async function compileAndOptionallyVerifyContracts(logger, contracts, cache //cacheConfig?: CacheConfig ) { //const cache = !cacheConfig ? undefined: await cacheFactory(cacheConfig); const entries = []; const mismatches = []; for (const c of contracts) { const { name, program, integrityHash } = c; logger.log(`Compiling ${name} contract/program.`); const timer = createTimer(); const compiled = await (cache ? program.compile({ cache }) : program.compile()); logger.log(`${name} compiled in ${timer()}`); const vk = compiled.verificationKey; const hashStr = vk.hash.toBigInt().toString(); logger.log(`${name} contract/program vk hash compiled: '${hashStr}'`); // Validate only if integrityHash is provided if (integrityHash && hashStr !== integrityHash) { mismatches.push(`${name}: Computed hash '${hashStr}' doesn't match expected hash '${integrityHash}'`); } const mappedKey = `${name}VerificationKey`; entries.push([mappedKey, vk]); } if (mismatches.length > 0) { const errorMessage = [ 'Verification key hash mismatch detected:', ...mismatches, '', `Refusing to start. Try clearing your o1js cache directory, typically found at '~/.cache/o1js'. Or do you need to run 'npm run bake-vk-hashes' and commit the changes?`, ].join('\n'); throw new Error(errorMessage); } logger.log('All contracts compiled successfully.'); return Object.fromEntries(entries); } // Timing utilities to replace console.time/timeEnd export function createTimer() { const start = Date.now(); return () => formatDuration(Date.now() - start); } export function formatDuration(ms) { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; const minutes = Math.floor(ms / 60000); const seconds = ((ms % 60000) / 1000).toFixed(2); return `${minutes}m ${seconds}s`; } //# sourceMappingURL=utils.js.map