UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

130 lines 5.94 kB
/** * SCRAM — Signature Commit-Reveal Authentication Mechanism * * Short: * SCRAM is a scheme where a Mina signature is Poseidon-hashed into a * commitment (codeChallenge). The codeChallenge is stored on-chain within * the Eth deposit smart contract as the key in the `lockedTokens` map. * Later, the claimant supplies the original * signature, publicKey, and message as private witnesses inside a ZK proof * on the Mina side. The ZK circuit verifies both that the signature is * valid for the claimed (publicKey, message) pair AND that its Poseidon * hash equals the stored codeChallenge — without exposing the signature * publicly. * * Definitions: * - signature: Mina Signature produced by signing a message with the * signer's private key. * - publicKey: Mina PublicKey corresponding to the signer's private key. * - message: Field[] — the message that was signed. * - codeChallenge : Field = Poseidon(signature.toFields()) — the commitment * stored on the Eth smart contract as the deposit key. * * Off-chain (committer): * 1. Sign a message with the signer's Mina private key. * 2. Compute codeChallenge = Poseidon(signature.toFields()). * 3. Submit the codeChallenge to the Eth smart contract's lockTokens * function, where it becomes the deposit key in `lockedTokens`. * * On-chain Mina (ZK verification): * 1. Claimant supplies the original signature, publicKey, and message * as private witnesses to the ZK circuit. * 2. Circuit verifies signature.verify(publicKey, message), proving key * ownership and message integrity. * 3. Circuit computes Poseidon(signature.toFields()) and asserts equality * with the stored codeChallenge. * 4. The signature is never exposed publicly — all verification occurs * provably inside the circuit. * * Security: * - Commitment is binding: only the holder of the original signature can * produce a preimage that hashes to the stored codeChallenge. * - Identity is verified: the signature must be valid for the claimed * publicKey and message, preventing substitution. * - Zero-knowledge: the signature witness is never revealed publicly, * preserving privacy of the commitment preimage. * - Poseidon is collision-resistant in the Mina field, so forging a * different signature with the same hash is computationally infeasible. * - Note: SCRAM does not enforce any constraint on what the message is. * Any domain separation or message binding is the caller's responsibility. * */ import { Poseidon, Signature, Field, Bytes, Struct, Provable, } from 'o1js'; import { wordToBytes } from '@nori-zk/proof-conversion/min'; const SCRAMMessage = Provable.Array(Field, 128); /** * The user-supplied private witness for SCRAM verification. * * Contains only the inputs that are not computed or derived on-chain: * - signature: the Mina Signature that was committed to. * - message: the Field[128] message that was signed. * * The codeChallenge is read from on-chain state and the publicKey is * derived from `this.sender` — neither is user input. */ export class SCRAMWitness extends Struct({ signature: Signature, message: SCRAMMessage, }) { } /** * Computes the SCRAM codeChallenge (commitment) from a Mina signature. * * @param signature - Mina Signature to commit to. * @returns Field representing the codeChallenge. * * Conceptually: * The signature is decomposed into fields and hashed via Poseidon to * produce a commitment. This codeChallenge is submitted to the Eth smart * contract's lockTokens function as the deposit key. The original * signature is required to open the commitment later inside a ZK proof * on the Mina side. */ export function createCodeChallenge(signature) { return Poseidon.hash(signature.toFields()); } /** * Verifies a SCRAM codeChallenge against a privately witnessed signature. * * @param challenge - Stored codeChallenge to verify against. * @param signature - The Mina Signature supplied as a private witness. * @param publicKey - Mina PublicKey of the original signer. * @param message - The message Field[] that was signed. * * Conceptually: * Two independent checks are performed inside the ZK circuit: * 1. Signature validity — the signature must verify against the supplied * publicKey and message, proving key ownership and message integrity. * 2. Commitment match — the Poseidon hash of the signature must equal the * stored codeChallenge, proving this is the exact signature that was * committed to. * The signature is never exposed publicly. */ export function verifyCodeChallenge(codeChallenge, signature, publicKey, message) { const signatureHash = Poseidon.hash(signature.toFields()); signature .verify(publicKey, message) .assertTrue('SCRAM signature verification failure'); signatureHash .equals(codeChallenge) .assertTrue('SCRAM codeChallenge does not equal signature hash'); } /** * Converts a SCRAM codeChallenge Field into a big-endian hex string. * * @param codeChallenge - The Poseidon Field representing the codeChallenge. * @returns A 0x-prefixed hexadecimal string representing the codeChallenge in * big-endian byte order, suitable for off-chain use or contract arguments. * * Conceptually: * - The codeChallenge Field is first converted into a 32-byte word. * - The bytes are reversed to switch from little-endian (internal representation) * to big-endian. * - The resulting bytes are serialized as a hex string with a 0x prefix. */ export function codeChallengeFieldToBEHex(codeChallenge) { const beCodeChallengeBytes = Bytes.from(wordToBytes(codeChallenge, 32).reverse()); const codeChallengeBEHex = `0x${beCodeChallengeBytes.toHex()}`; return codeChallengeBEHex; } //# sourceMappingURL=scram.js.map