@nori-zk/o1js-zk-utils
Version:
o1js-zk-utils supporting Nori Bridge
309 lines • 12.8 kB
JavaScript
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