@nori-zk/mina-token-bridge
Version:
Nori ethereum state settelment and nETH token bridge zkApp
208 lines • 10.2 kB
JavaScript
import { createTimer, Bytes32, computeMerkleTreeDepthAndSize, getMerklePathFromLeaves, getMerkleZeros, } from '@nori-zk/o1js-zk-utils';
import { DynamicArray } from '@nori-zk/mina-attestations/dynamic/array';
import { Bytes, Field, Poseidon, Provable, Struct, UInt64, UInt8 } from 'o1js';
import { Logger } from 'esm-iso-logger';
// ------- Deposit attestation ---------------------------------
const logger = new Logger('DepositAttestation');
export class ContractDeposit extends Struct({
codeChallenge: Bytes32.provable,
value: Bytes32.provable,
}) {
}
const treeDepth = 16;
export const MerklePath = DynamicArray(Field, { maxLength: treeDepth });
export class MerkleTreeContractDepositAttestorInput extends Struct({
path: MerklePath,
index: UInt64,
value: ContractDeposit,
}) {
}
export function buildMerkleTreeContractDepositAttestorInput(jsonInputs) {
const merklePath = MerklePath.from([]);
jsonInputs.path.forEach((element) => merklePath.push(new Field(BigInt(element))));
return new MerkleTreeContractDepositAttestorInput({
path: merklePath,
index: UInt64.fromValue(jsonInputs.depositIndex),
value: new ContractDeposit({
codeChallenge: Bytes32.fromHex(jsonInputs.despositSlotRaw.slot_key_code_challenge.slice(2)),
value: Bytes32.fromHex(jsonInputs.despositSlotRaw.value.slice(2)),
}),
});
}
export function provableStorageSlotLeafHash(contractDeposit) {
const codeChallengeBytes = contractDeposit.codeChallenge.bytes; // UInt8[]
const valueBytes = contractDeposit.value.bytes; // UInt8[]
// 64 bytes total (32 + 32), max 31 bytes per field → 3 fields
// Strip high byte off each, pack into firstField; remaining 31 bytes each in secondField/thirdField
// firstFieldBytes: 1 byte from codeChallengeBytes and 1 byte from valueBytes
const firstFieldBytes = [];
firstFieldBytes.push(codeChallengeBytes[0]);
firstFieldBytes.push(valueBytes[0]);
for (let i = 2; i < 32; i++) {
firstFieldBytes.push(UInt8.zero); // static pad to 32
}
// secondFieldBytes: remaining 31 bytes from codeChallengeBytes (1 to 31)
const secondFieldBytes = [];
for (let i = 1; i < 32; i++) {
secondFieldBytes.push(codeChallengeBytes[i]);
}
// already 31 elements; add 1 zero to reach 32
secondFieldBytes.push(UInt8.zero);
// thirdFieldBytes: remaining 31 bytes from valueBytes (1 to 31)
const thirdFieldBytes = [];
for (let i = 1; i < 32; i++) {
thirdFieldBytes.push(valueBytes[i]);
}
// already 31 elements; add 1 zero to reach 32
thirdFieldBytes.push(UInt8.zero);
// Convert UInt8[] to Bytes (provable bytes)
const firstBytes = Bytes.from(firstFieldBytes);
const secondBytes = Bytes.from(secondFieldBytes);
const thirdBytes = Bytes.from(thirdFieldBytes);
// Little endian
let firstField = new Field(0);
let secondField = new Field(0);
let thirdField = new Field(0);
for (let i = 31; i >= 0; i--) {
firstField = firstField.mul(256).add(firstBytes.bytes[i].value);
secondField = secondField.mul(256).add(secondBytes.bytes[i].value);
thirdField = thirdField.mul(256).add(thirdBytes.bytes[i].value);
}
return Poseidon.hash([firstField, secondField, thirdField]);
}
export function getContractDepositSlotRootFromContractDepositAndWitness(input) {
let { index, path } = input;
let currentHash = provableStorageSlotLeafHash(input.value);
const bitPath = index.value.toBits(path.maxLength);
path.forEach((sibling, isDummy, i) => {
const bit = bitPath[i];
const left = Provable.if(bit, Field, sibling, currentHash);
const right = Provable.if(bit, Field, currentHash, sibling);
const nextHash = Poseidon.hash([left, right]);
/*Provable.asProver(() => {
if (!isDummy) {
console.log(
`merkle pair @ level ${i}:`,
'left =',
typeof left.toBigInt === 'function'
? left.toBigInt()
: left,
'right =',
typeof right.toBigInt === 'function'
? right.toBigInt()
: right
);
}
});*/
currentHash = Provable.if(isDummy, Field, currentHash, nextHash);
});
return currentHash;
}
export function extractCodeChallengeAndTotalLocked(merkleTreeContractDepositAttestorInput) {
// Unpack deposit
const deposit = merkleTreeContractDepositAttestorInput.value;
// Convert deposit.codeChallenge from Bytes32 into a Field
const codeChallengeBytes = deposit.codeChallenge.bytes;
let codeChallenge = new Field(0);
for (let i = 0; i < 32; i++) {
codeChallenge = codeChallenge.mul(256).add(codeChallengeBytes[i].value);
}
Provable.asProver(() => {
logger.log('deposit value bytes');
logger.log(deposit.value.bytes.map((byte) => byte.toBigInt().toString()));
logger.log('codeChallenge');
logger.log(codeChallenge.toBigInt());
});
// Turn totalLocked into a field
const totalLockedBytes = deposit.value.bytes;
let totalLocked = new Field(0);
for (let i = 0; i < 32; i++) {
totalLocked = totalLocked.mul(256).add(totalLockedBytes[i].value);
}
return {
totalLocked,
codeChallenge,
};
}
export function buildContractDepositSlotLeaves(contractDeposits) {
return contractDeposits.map((leaf) => provableStorageSlotLeafHash(leaf));
}
export function getMerklePathFromContractDeposits(merkleLeaves, index) {
const nLeaves = merkleLeaves.length;
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const path = getMerklePathFromLeaves(merkleLeaves, paddedSize, depth, index, getMerkleZeros(depth));
const merklePath = MerklePath.from([]);
path.forEach((element) => merklePath.push(element));
return merklePath;
}
async function proofConversionServiceRequest(depositBlockNumber, domain = 'https://pcs.nori.it.com') {
const fetchResponse = await fetch(`${domain}/converted-consensus-mpt-proofs/${depositBlockNumber}`);
logger.log('fetchResponse GET', fetchResponse);
const json = await fetchResponse.json();
logger.log('parsedjson', json, typeof json);
if ('error' in json)
throw new Error(json.error);
return json;
}
async function fetchContractWindowSlotProofs(depositBlockNumber, domain = 'https://pcs.nori.it.com') {
logger.log(`Fetching proof bundle for deposit with block number: ${depositBlockNumber}`);
const proofConversionTimer = createTimer();
const { consensusMPTProof: { proof: consensusMPTProofProof, contract_storage_slots: consensusMPTProofContractStorageSlots, }, consensusMPTProofVerification: consensusMPTProofVerification, } = await proofConversionServiceRequest(depositBlockNumber, domain);
logger.log(`proofConversionServiceRequest: ${proofConversionTimer()}`);
logger.log('consensusMPTProofVerification, consensusMPTProofProof, consensusMPTProofContractStorageSlots', consensusMPTProofVerification, consensusMPTProofProof, consensusMPTProofContractStorageSlots);
return {
consensusMPTProofProof,
consensusMPTProofContractStorageSlots,
consensusMPTProofVerification,
};
}
export async function computeDepositAttestationWitness(depositBlockNumber, codeChallengeBEHex, domain = 'https://pcs.nori.it.com') {
const { consensusMPTProofContractStorageSlots } = await fetchContractWindowSlotProofs(depositBlockNumber, domain);
// Find deposit
logger.log(`Finding deposit within bundle.consensusMPTProof.contract_storage_slots`);
const paddedConsensusMPTProofContractStorageSlots = (consensusMPTProofContractStorageSlots).map((slot) => {
return {
//prettier-ignore
slot_key_code_challenge: `0x${slot.slot_key_code_challenge.slice(2).padStart(64, '0')}`,
//prettier-ignore
value: `0x${slot.value.slice(2).padStart(64, '0')}`,
};
});
const depositIndex = paddedConsensusMPTProofContractStorageSlots.findIndex((slot) => slot.slot_key_code_challenge === codeChallengeBEHex);
if (depositIndex === -1)
throw new Error(`Could not find deposit index with codeChallengeBEHex: ${codeChallengeBEHex} in slots ${JSON.stringify(paddedConsensusMPTProofContractStorageSlots, null, 4)}`);
logger.log(`Found deposit within bundle.consensusMPTProof.contract_storage_slots`);
const despositSlotRaw = paddedConsensusMPTProofContractStorageSlots[depositIndex];
const totalDespositedValue = despositSlotRaw.value;
logger.log(`Total deposited to date (hex): ${totalDespositedValue}`);
// Build contract storage slots (to be hashed)
const contractStorageSlots = paddedConsensusMPTProofContractStorageSlots.map((slot) => {
const codeChallenge = slot.slot_key_code_challenge;
const value = slot.value;
logger.log({ codeChallenge, value });
return new ContractDeposit({
codeChallenge: Bytes32.fromHex(codeChallenge.slice(2)),
value: Bytes32.fromHex(value.slice(2)),
});
});
// Build deposit witness
// Build leaves
const buildLeavesTimer = createTimer();
const leaves = buildContractDepositSlotLeaves(contractStorageSlots);
logger.log(`buildContractDepositLeaves: ${buildLeavesTimer()}`);
logger.log('leaves', leaves.map((leaf) => leaf.toBigInt()));
// Compute path
const merklePathTimer = createTimer();
const nLeaves = leaves.length;
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const path = getMerklePathFromLeaves([...leaves], paddedSize, depth, depositIndex, getMerkleZeros(depth));
logger.log(`getContractDepositWitness: ${merklePathTimer()}`);
logger.log('path', path.map((pathEle) => pathEle.toBigInt()));
logger.log(`All inputs built needed to compute mint proof!`);
return {
path: path.map((it) => it.toBigInt().toString()),
depositIndex,
despositSlotRaw,
};
}
//# sourceMappingURL=depositAttestation.js.map