From 879d2dbe3592ba5e56e16f066e7e405a9e8b9c0e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 10:53:18 -0400 Subject: [PATCH 01/23] Feat: remove unused input generation scripts for transfer and unshield circuits --- scripts/generators/generate_input.ts | 274 ------------------ ...enerate_unshield_and_private_link_input.js | 149 ---------- 2 files changed, 423 deletions(-) delete mode 100644 scripts/generators/generate_input.ts delete mode 100644 scripts/generators/generate_unshield_and_private_link_input.js diff --git a/scripts/generators/generate_input.ts b/scripts/generators/generate_input.ts deleted file mode 100644 index 734ee12..0000000 --- a/scripts/generators/generate_input.ts +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Generate valid input.json for transfer circuit benchmarks - * - * This script generates cryptographically valid witness data including: - * - EdDSA key pairs and signatures - * - Merkle tree with valid paths - * - Proper nullifiers and commitments - * - * Usage: npx ts-node scripts/generate_input.ts - */ - -import { buildPoseidon, buildEddsa, buildBabyjub } from "circomlibjs"; -import * as fs from "fs"; -import * as path from "path"; -import * as crypto from "crypto"; - -const MERKLE_TREE_DEPTH = 20; - -interface InputNote { - value: bigint; - asset_id: bigint; - owner_pubkey: any; - blinding: bigint; - spending_key: bigint; -} - -interface OutputNote { - value: bigint; - asset_id: bigint; - owner_pubkey: any; - blinding: bigint; -} - -interface CircuitInput { - merkle_root: string; - nullifiers: string[]; - commitments: string[]; - input_values: string[]; - input_asset_ids: string[]; - input_blindings: string[]; - spending_keys: string[]; - input_owner_Ax: string[]; - input_owner_Ay: string[]; - input_sig_R8x: string[]; - input_sig_R8y: string[]; - input_sig_S: string[]; - input_path_elements: string[][]; - input_path_indices: number[][]; - output_values: string[]; - output_asset_ids: string[]; - output_owner_pubkeys: string[]; - output_blindings: string[]; - [key: string]: any; -} - -// Utility to convert Poseidon output to string -const poseidonToStr = (F: any, hash: any): string => F.toString(hash); - -async function main(): Promise { - console.log("=== Transfer Circuit Input Generator ===\n"); - - // Initialize crypto primitives - console.log("1. Initializing crypto primitives..."); - const poseidon = await buildPoseidon(); - const eddsa = await buildEddsa(); - const babyJub = await buildBabyjub(); - const F = poseidon.F; - - // Generate EdDSA key pairs - console.log("\n2. Generating EdDSA key pairs..."); - const privKey1 = crypto.randomBytes(32); - const privKey2 = crypto.randomBytes(32); - - const pubKey1 = eddsa.prv2pub(privKey1); - const pubKey2 = eddsa.prv2pub(privKey2); - - console.log(" Key 1 Ax:", F.toString(pubKey1[0]).slice(0, 20) + "..."); - console.log(" Key 2 Ax:", F.toString(pubKey2[0]).slice(0, 20) + "..."); - - // Zero value for empty merkle leaves - const ZERO = BigInt(0); - - // Compute zero hashes for merkle tree padding - let zeroHashes: bigint[] = [ZERO]; - for (let i = 1; i <= MERKLE_TREE_DEPTH; i++) { - const prevZero = zeroHashes[i - 1]; - zeroHashes.push(BigInt(poseidonToStr(F, poseidon([prevZero, prevZero])))); - } - - // Input notes - console.log("\n3. Creating input notes..."); - const input1: InputNote = { - value: BigInt(100), - asset_id: BigInt(0), - owner_pubkey: pubKey1[0], // Ax is the owner pubkey - blinding: BigInt("11111111111111111111"), - spending_key: BigInt("99999999999999999999"), - }; - - const input2: InputNote = { - value: BigInt(50), - asset_id: BigInt(0), - owner_pubkey: pubKey2[0], - blinding: BigInt("22222222222222222222"), - spending_key: BigInt("99999999999999999999"), - }; - - // Compute input commitments - const inputCommitment1 = poseidon([ - input1.value, - input1.asset_id, - input1.owner_pubkey, - input1.blinding, - ]); - const inputCommitment2 = poseidon([ - input2.value, - input2.asset_id, - input2.owner_pubkey, - input2.blinding, - ]); - - const c1 = BigInt(poseidonToStr(F, inputCommitment1)); - const c2 = BigInt(poseidonToStr(F, inputCommitment2)); - - console.log(" Commitment 1:", c1.toString().slice(0, 20) + "..."); - console.log(" Commitment 2:", c2.toString().slice(0, 20) + "..."); - - // Sign commitments with EdDSA - console.log("\n4. Signing commitments with EdDSA..."); - const msg1 = F.e(c1); - const msg2 = F.e(c2); - - const sig1 = eddsa.signPoseidon(privKey1, msg1); - const sig2 = eddsa.signPoseidon(privKey2, msg2); - - console.log(" Signature 1 R8x:", F.toString(sig1.R8[0]).slice(0, 20) + "..."); - console.log(" Signature 2 R8x:", F.toString(sig2.R8[0]).slice(0, 20) + "..."); - - // Verify signatures locally - const valid1 = eddsa.verifyPoseidon(msg1, sig1, pubKey1); - const valid2 = eddsa.verifyPoseidon(msg2, sig2, pubKey2); - console.log(" Signature 1 valid:", valid1); - console.log(" Signature 2 valid:", valid2); - - if (!valid1 || !valid2) { - console.error("❌ EdDSA signature verification failed!"); - process.exit(1); - } - - // Build Merkle tree - console.log("\n5. Building Merkle tree..."); - - // Level 0 hash: h01 = poseidon(c1, c2) - const h01 = BigInt(poseidonToStr(F, poseidon([c1, c2]))); - - // Build the rest of the tree upward - let computedHashes: bigint[] = [h01]; - for (let i = 1; i < MERKLE_TREE_DEPTH; i++) { - const prevHash = computedHashes[i - 1]; - const nextHash = BigInt(poseidonToStr(F, poseidon([prevHash, zeroHashes[i]]))); - computedHashes.push(nextHash); - } - const merkleRoot = computedHashes[MERKLE_TREE_DEPTH - 1]; - console.log(" Merkle Root:", merkleRoot.toString().slice(0, 20) + "..."); - - // Build merkle paths - // Circuit path_index semantics: 0 = right child, 1 = left child - console.log("\n6. Building merkle paths..."); - - // For c1 (index 0): left child at level 0 - const merkle_path_elements_1 = [c2.toString()]; - const merkle_path_indices_1 = [0]; // c1 is LEFT, so index = 0 - - for (let i = 1; i < MERKLE_TREE_DEPTH; i++) { - merkle_path_elements_1.push(zeroHashes[i].toString()); - merkle_path_indices_1.push(0); // always on LEFT - } - - // For c2 (index 1): right child at level 0 - const merkle_path_elements_2 = [c1.toString()]; - const merkle_path_indices_2 = [1]; // c2 is RIGHT, so index = 1 - - for (let i = 1; i < MERKLE_TREE_DEPTH; i++) { - merkle_path_elements_2.push(zeroHashes[i].toString()); - merkle_path_indices_2.push(0); // always on LEFT - } - - // Compute nullifiers - console.log("\n7. Computing nullifiers..."); - const nullifier1 = poseidon([inputCommitment1, input1.spending_key]); - const nullifier2 = poseidon([inputCommitment2, input2.spending_key]); - - console.log(" Nullifier 1:", poseidonToStr(F, nullifier1).slice(0, 20) + "..."); - console.log(" Nullifier 2:", poseidonToStr(F, nullifier2).slice(0, 20) + "..."); - - // Output notes (must sum to same value: 100 + 50 = 80 + 70) - console.log("\n8. Creating output notes..."); - const output1: OutputNote = { - value: BigInt(80), - asset_id: BigInt(0), - owner_pubkey: BigInt("98765432109876543210"), // Bob - blinding: BigInt("33333333333333333333"), - }; - - const output2: OutputNote = { - value: BigInt(70), - asset_id: BigInt(0), - owner_pubkey: F.toObject(pubKey1[0]), // Alice gets change - blinding: BigInt("44444444444444444444"), - }; - - const outputCommitment1 = poseidon([ - output1.value, - output1.asset_id, - output1.owner_pubkey, - output1.blinding, - ]); - const outputCommitment2 = poseidon([ - output2.value, - output2.asset_id, - output2.owner_pubkey, - output2.blinding, - ]); - - // Prepare circuit input - const input: CircuitInput = { - // Public inputs - merkle_root: merkleRoot.toString(), - nullifiers: [poseidonToStr(F, nullifier1), poseidonToStr(F, nullifier2)], - commitments: [poseidonToStr(F, outputCommitment1), poseidonToStr(F, outputCommitment2)], - - // Private inputs - input notes - input_values: [input1.value.toString(), input2.value.toString()], - input_asset_ids: [input1.asset_id.toString(), input2.asset_id.toString()], - input_blindings: [input1.blinding.toString(), input2.blinding.toString()], - spending_keys: [input1.spending_key.toString(), input2.spending_key.toString()], - - // EdDSA public keys (Ax, Ay) - input_owner_Ax: [F.toString(pubKey1[0]), F.toString(pubKey2[0])], - input_owner_Ay: [F.toString(pubKey1[1]), F.toString(pubKey2[1])], - - // EdDSA signatures (R8x, R8y, S) - input_sig_R8x: [F.toString(sig1.R8[0]), F.toString(sig2.R8[0])], - input_sig_R8y: [F.toString(sig1.R8[1]), F.toString(sig2.R8[1])], - input_sig_S: [sig1.S.toString(), sig2.S.toString()], - - // Merkle paths - input_path_elements: [merkle_path_elements_1, merkle_path_elements_2], - input_path_indices: [merkle_path_indices_1, merkle_path_indices_2], - - // Output notes - output_values: [output1.value.toString(), output2.value.toString()], - output_asset_ids: [output1.asset_id.toString(), output2.asset_id.toString()], - output_owner_pubkeys: [output1.owner_pubkey.toString(), output2.owner_pubkey.toString()], - output_blindings: [output1.blinding.toString(), output2.blinding.toString()], - }; - - // Save input to circuits/build/ directory (not scripts/build/) - const buildDir = path.join(__dirname, "..", "..", "build"); - if (!fs.existsSync(buildDir)) { - fs.mkdirSync(buildDir, { recursive: true }); - } - - fs.writeFileSync(path.join(buildDir, "input.json"), JSON.stringify(input, null, 2)); - - console.log("\n✅ Saved input.json to build/input.json"); - console.log("\n=== DONE ==="); -} - -main().catch((err) => { - console.error("❌ Error:", err.message); - process.exit(1); -}); diff --git a/scripts/generators/generate_unshield_and_private_link_input.js b/scripts/generators/generate_unshield_and_private_link_input.js deleted file mode 100644 index 1e54b8f..0000000 --- a/scripts/generators/generate_unshield_and_private_link_input.js +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env node -/** - * Generates valid input JSON files for the unshield and private_link circuits. - * Uses circomlibjs (already installed) for Poseidon hashing and Merkle trees. - * - * Usage: - * node scripts/generators/generate_unshield_and_private_link_input.js - * - * Outputs: - * build/unshield_input.json - * build/private_link_input.json - */ -"use strict"; - -const { buildPoseidon } = require("circomlibjs"); -const crypto = require("crypto"); -const fs = require("fs"); -const path = require("path"); - -const TREE_DEPTH = 20; -const BUILD_DIR = path.join(__dirname, "../../build"); - -// ── Merkle tree helpers ────────────────────────────────────────────────────── - -function buildMerkleTree(poseidon, F, leaves) { - // Pad to next power-of-two of size 2^TREE_DEPTH - const size = 1 << TREE_DEPTH; - const zero = F.zero; - const nodes = new Array(size * 2).fill(zero); - - for (let i = 0; i < leaves.length; i++) { - nodes[size + i] = leaves[i]; - } - for (let i = size - 1; i >= 1; i--) { - nodes[i] = poseidon([nodes[i * 2], nodes[i * 2 + 1]]); - } - return nodes; -} - -function getMerklePath(nodes, leafIdx) { - const size = 1 << TREE_DEPTH; - const elements = []; - const indices = []; - let idx = size + leafIdx; - for (let d = 0; d < TREE_DEPTH; d++) { - const sibling = idx % 2 === 0 ? idx + 1 : idx - 1; - elements.push(nodes[sibling]); - indices.push(idx % 2 === 0 ? 0 : 1); - idx = Math.floor(idx / 2); - } - return { elements, indices }; -} - -// ── Main ───────────────────────────────────────────────────────────────────── - -async function main() { - const poseidon = await buildPoseidon(); - const F = poseidon.F; - - const fStr = (x) => F.toString(x); - - // ── Unshield input ──────────────────────────────────────────────────────── - - console.log("=== Generating unshield input ==="); - - const note_value = BigInt(1000); - const note_asset_id = BigInt(0); - const note_owner = BigInt("12345678901234567890"); // arbitrary pubkey scalar - const note_blinding = BigInt("0x" + crypto.randomBytes(31).toString("hex")) % F.p; - const spending_key = BigInt("0x" + crypto.randomBytes(31).toString("hex")) % F.p; - - // commit = Poseidon(value, asset_id, owner, blinding) - const commitment = poseidon([ - F.e(note_value), - F.e(note_asset_id), - F.e(note_owner), - F.e(note_blinding), - ]); - - // nullifier = Poseidon(commitment, spending_key) - const nullifier = poseidon([commitment, F.e(spending_key)]); - - // Build a minimal Merkle tree with the commitment at leaf 0 - const leaves = [commitment]; - const treeNodes = buildMerkleTree(poseidon, F, leaves); - const { elements: path_elements, indices: path_indices } = getMerklePath(treeNodes, 0); - const merkle_root = treeNodes[1]; - - const recipient = BigInt("42"); // non-zero recipient address - const asset_id = note_asset_id; - const amount = note_value; // unshield reveals exact amount - - const unshieldInput = { - merkle_root: fStr(merkle_root), - nullifier: fStr(nullifier), - amount: amount.toString(), - recipient: recipient.toString(), - asset_id: asset_id.toString(), - - note_value: note_value.toString(), - note_asset_id: note_asset_id.toString(), - note_owner: note_owner.toString(), - note_blinding: fStr(F.e(note_blinding)), - spending_key: fStr(F.e(spending_key)), - - path_elements: path_elements.map(fStr), - path_indices: path_indices, - }; - - const unshieldPath = path.join(BUILD_DIR, "unshield_input.json"); - fs.writeFileSync(unshieldPath, JSON.stringify(unshieldInput, null, 2)); - console.log(` ✓ Saved: ${unshieldPath}`); - console.log(` commitment: ${fStr(commitment).slice(0, 20)}...`); - console.log(` merkle_root: ${fStr(merkle_root).slice(0, 20)}...`); - - // ── Private link input ──────────────────────────────────────────────────── - - console.log("\n=== Generating private_link input ==="); - - const chain_id_fe = BigInt(1); // chain id = 1 (Ethereum mainnet style) - const address_fe = BigInt("0x" + crypto.randomBytes(20).toString("hex")); // 20-byte address - const blinding_fe = BigInt("0x" + crypto.randomBytes(31).toString("hex")) % F.p; - const call_hash_fe = BigInt("0x" + crypto.randomBytes(31).toString("hex")) % F.p; - - // commitment = Poseidon(Poseidon(chain_id_fe, address_fe), blinding_fe) - const inner = poseidon([F.e(chain_id_fe), F.e(address_fe)]); - const pl_commitment = poseidon([inner, F.e(blinding_fe)]); - - const privateLinkInput = { - commitment: fStr(pl_commitment), - call_hash_fe: fStr(F.e(call_hash_fe)), - - chain_id_fe: chain_id_fe.toString(), - address_fe: fStr(F.e(address_fe)), - blinding_fe: fStr(F.e(blinding_fe)), - }; - - const plPath = path.join(BUILD_DIR, "private_link_input.json"); - fs.writeFileSync(plPath, JSON.stringify(privateLinkInput, null, 2)); - console.log(` ✓ Saved: ${plPath}`); - console.log(` commitment: ${fStr(pl_commitment).slice(0, 20)}...`); - - console.log("\n✅ Done"); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); From d6cc0b04fd012514016411e590230b6acfe1b7f7 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 19:31:37 -0400 Subject: [PATCH 02/23] Feat: add value proof circuit to ensure commitment integrity in relayer fee claim --- circuits/disclosure.circom | 151 ------------------------------------ circuits/value_proof.circom | 56 +++++++++++++ 2 files changed, 56 insertions(+), 151 deletions(-) delete mode 100644 circuits/disclosure.circom create mode 100644 circuits/value_proof.circom diff --git a/circuits/disclosure.circom b/circuits/disclosure.circom deleted file mode 100644 index bca7c15..0000000 --- a/circuits/disclosure.circom +++ /dev/null @@ -1,151 +0,0 @@ -pragma circom 2.0.0; - -include "./note.circom"; -include "../node_modules/circomlib/circuits/poseidon.circom"; -include "../node_modules/circomlib/circuits/bitify.circom"; -include "../node_modules/circomlib/circuits/escalarmulfix.circom"; -include "../node_modules/circomlib/circuits/escalarmulany.circom"; - -// Returns true_value if condition=1, false_value if condition=0. -// Enforces condition is boolean. -template Selector() { - signal input condition; - signal input true_value; - signal input false_value; - signal output out; - - condition * (condition - 1) === 0; - - signal inv_condition; - inv_condition <== 1 - condition; - - signal term1; - signal term2; - term1 <== condition * true_value; - term2 <== inv_condition * false_value; - - out <== term1 + term2; -} - -// Proves knowledge of a note preimage that opens the given commitment and -// selectively reveals chosen fields, encrypted with the auditor's Baby Jubjub -// public key using ECDH + Poseidon stream cipher. -// -// Encryption scheme (all arithmetic in BN254 scalar field): -// r <- private ephemeral scalar (random per disclosure) -// epk = r · G (Baby Jubjub base point, public output) -// shared = r · pk_A (ECDH shared secret, private witness) -// k_i = Poseidon(shared.x, shared.y, i) (keystream PRF) -// enc_f = revealed_f + k_i (one-time pad in the field) -// -// Decryption (off-chain, only auditor with sk_A where pk_A = sk_A · G): -// shared = sk_A · epk (same shared secret by ECDH symmetry) -// revealed_f = enc_f - k_i (mod BN254 prime) -// -// Public signals (8 field elements = 256 bytes on-chain): -// commitment, auditor_pk_x, auditor_pk_y, -// epk_x, epk_y, enc_value, enc_asset_id, enc_owner_hash -template SelectiveDisclosure() { - // ── Public inputs ───────────────────────────────────────────────────── - signal input commitment; - signal input auditor_pk_x; // Baby Jubjub X-coord of auditor public key - signal input auditor_pk_y; // Baby Jubjub Y-coord of auditor public key - - // ── Public outputs (ciphertext — runtime verifies, only auditor decrypts) - signal output epk_x; // r·G ephemeral public key X - signal output epk_y; // r·G ephemeral public key Y - signal output enc_value; // revealed_value + k0 (field element) - signal output enc_asset_id; // revealed_asset_id + k1 - signal output enc_owner_hash; // Poseidon(owner_pubkey) + k2 (or k2 if hidden) - - // ── Private inputs ──────────────────────────────────────────────────── - signal input value; - signal input asset_id; - signal input owner_pubkey; - signal input blinding; - signal input disclose_value; // 1 = reveal, 0 = hide - signal input disclose_asset_id; - signal input disclose_owner; - signal input r; // ephemeral scalar (random, kept secret) - - // Constraint 1: commitment == Poseidon(value, asset_id, owner_pubkey, blinding) - component note_commitment = NoteCommitment(); - note_commitment.value <== value; - note_commitment.asset_id <== asset_id; - note_commitment.owner_pubkey <== owner_pubkey; - note_commitment.blinding <== blinding; - note_commitment.commitment === commitment; - - // Constraint 2: disclosure masks must be boolean - disclose_value * (disclose_value - 1) === 0; - disclose_asset_id * (disclose_asset_id - 1) === 0; - disclose_owner * (disclose_owner - 1) === 0; - - // Constraint 3: plaintext value (0 if hidden) - component value_selector = Selector(); - value_selector.condition <== disclose_value; - value_selector.true_value <== value; - value_selector.false_value <== 0; - - // Constraint 4: plaintext asset_id (0 if hidden) - component asset_selector = Selector(); - asset_selector.condition <== disclose_asset_id; - asset_selector.true_value <== asset_id; - asset_selector.false_value <== 0; - - // Constraint 5: owner hash = Poseidon(owner_pubkey), or 0 if hidden - // Raw pubkey is never revealed — only its hash. - component owner_hasher = Poseidon(1); - owner_hasher.inputs[0] <== owner_pubkey; - - component owner_selector = Selector(); - owner_selector.condition <== disclose_owner; - owner_selector.true_value <== owner_hasher.out; - owner_selector.false_value <== 0; - - // Constraint 6: ephemeral public key epk = r · G (fixed base) - var BASE8[2] = [ - 5299619240641551281634865583518297030282874472190772894086521144482721001553, - 16950150798460657717958625567821834550301663161624707787222815936182638968203 - ]; - component r_bits = Num2Bits(253); - r_bits.in <== r; - - component epk_mul = EscalarMulFix(253, BASE8); - for (var i = 0; i < 253; i++) { - epk_mul.e[i] <== r_bits.out[i]; - } - epk_x <== epk_mul.out[0]; - epk_y <== epk_mul.out[1]; - - // Constraint 7: shared secret shared = r · pk_A (variable base) - component shared_mul = EscalarMulAny(253); - for (var i = 0; i < 253; i++) { - shared_mul.e[i] <== r_bits.out[i]; - } - shared_mul.p[0] <== auditor_pk_x; - shared_mul.p[1] <== auditor_pk_y; - - // Constraint 8: Poseidon keystream from shared secret - component k0 = Poseidon(3); - k0.inputs[0] <== shared_mul.out[0]; - k0.inputs[1] <== shared_mul.out[1]; - k0.inputs[2] <== 0; - - component k1 = Poseidon(3); - k1.inputs[0] <== shared_mul.out[0]; - k1.inputs[1] <== shared_mul.out[1]; - k1.inputs[2] <== 1; - - component k2 = Poseidon(3); - k2.inputs[0] <== shared_mul.out[0]; - k2.inputs[1] <== shared_mul.out[1]; - k2.inputs[2] <== 2; - - // Constraint 9: ciphertext = plaintext + keystream (field addition, no overflow) - enc_value <== value_selector.out + k0.out; - enc_asset_id <== asset_selector.out + k1.out; - enc_owner_hash <== owner_selector.out + k2.out; -} - -component main {public [commitment, auditor_pk_x, auditor_pk_y]} = SelectiveDisclosure(); diff --git a/circuits/value_proof.circom b/circuits/value_proof.circom new file mode 100644 index 0000000..2106542 --- /dev/null +++ b/circuits/value_proof.circom @@ -0,0 +1,56 @@ +pragma circom 2.0.0; + +include "./note.circom"; +include "../node_modules/circomlib/circuits/poseidon.circom"; + +// Proves knowledge of a note preimage that opens the given commitment. +// +// Used by pallet-shielded-pool::claim_shielded_fees to guarantee that +// the commitment inserted into the Merkle tree encodes exactly the declared +// (value, asset_id) before any relay-fee accounting occurs. +// +// Without this proof a relayer could craft a commitment encoding a larger +// value (e.g. 10× the pending fees) and later unshield that inflated amount. +// +// Public signals layout (76 bytes on-chain): +// commitment[0..32] — field element, 32-byte LE +// value[32..40] — u64 LE (fits in BN254 scalar field) +// asset_id[40..44] — u32 LE +// owner_hash[44..76] — Poseidon(owner_pubkey), 32-byte LE +// +// The on-chain verifier checks: +// 1. public_signals[0..32] == commitment (arg match) +// 2. public_signals[32..40] == amount (value match, u64 LE) +// 3. public_signals[40..44] == asset_id (asset match, u32 LE) +// owner_hash is provided for off-chain audit but not enforced on-chain. +// +// Decryption / audit: owner_pubkey remains private; only its Poseidon hash +// is revealed, preventing linkage to the Baby Jubjub public key. +template ValueProof() { + // ── Public inputs ───────────────────────────────────────────────────── + signal input commitment; + signal input value; // u64 — must fit in BN254 scalar field + signal input asset_id; // u32 + + // ── Public outputs ──────────────────────────────────────────────────── + signal output owner_hash; // Poseidon(owner_pubkey) — auxiliary, not enforced + + // ── Private inputs ──────────────────────────────────────────────────── + signal input owner_pubkey; + signal input blinding; + + // Constraint 1: commitment == Poseidon(value, asset_id, owner_pubkey, blinding) + component note_commitment = NoteCommitment(); + note_commitment.value <== value; + note_commitment.asset_id <== asset_id; + note_commitment.owner_pubkey <== owner_pubkey; + note_commitment.blinding <== blinding; + note_commitment.commitment === commitment; + + // Constraint 2: owner_hash = Poseidon(owner_pubkey) — keeps pubkey private + component hasher = Poseidon(1); + hasher.inputs[0] <== owner_pubkey; + owner_hash <== hasher.out; +} + +component main {public [commitment, value, asset_id]} = ValueProof(); From dbdd4cd3e41c0078cbbb53604abbbcecf0c5ad51 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 19:31:52 -0400 Subject: [PATCH 03/23] Feat: add value proof circuit tests to validate commitment integrity and prevent inflation attacks --- test/disclosure.test.ts | 441 --------------------------------------- test/value_proof.test.ts | 275 ++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 441 deletions(-) delete mode 100644 test/disclosure.test.ts create mode 100644 test/value_proof.test.ts diff --git a/test/disclosure.test.ts b/test/disclosure.test.ts deleted file mode 100644 index bf0d824..0000000 --- a/test/disclosure.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -import path from "path"; -import fs from "fs"; -import { expect } from "chai"; -import { wasm as wasm_tester } from "circom_tester"; -import { buildPoseidon, buildBabyjub } from "circomlibjs"; -import type { WasmTester } from "circom_tester"; - -// ─── Types ──────────────────────────────────────────────────────────────────── - -interface CircuitInput extends Record { - // Public inputs - commitment: string; - auditor_pk_x: string; - auditor_pk_y: string; - // Private inputs - value: string; - asset_id: string; - owner_pubkey: string; - blinding: string; - disclose_value: string; - disclose_asset_id: string; - disclose_owner: string; - r: string; -} - -interface CircuitOutputs { - epk_x: bigint; - epk_y: bigint; - enc_value: bigint; - enc_asset_id: bigint; - enc_owner_hash: bigint; -} - -// ─── Constants ──────────────────────────────────────────────────────────────── - -const BN254_P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n; - -// Fixed test scalars (within BN254 scalar field, not full random to be deterministic) -const TEST_AUDITOR_SK = 123456789012345678901234567890123456789n; -const TEST_R = 987654321098765432109876543210987654321n; - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -describe("Selective Disclosure Circuit — ECDH on-circuit", function () { - this.timeout(120000); - - const circuitPath = path.join(__dirname, "..", "circuits", "disclosure.circom"); - const outputDir = path.join(__dirname, "..", "build"); - - let circuit: WasmTester; - let poseidon: any; - let F: any; - let babyJub: any; - - // Baby Jubjub auditor keypair (derived from fixed sk for determinism) - let auditorPkX: bigint; - let auditorPkY: bigint; - - before(async function () { - const precompiledWasm = path.join(outputDir, "disclosure_js", "disclosure.wasm"); - if (!fs.existsSync(precompiledWasm)) { - this.skip(); - return; - } - - circuit = await wasm_tester(circuitPath, { output: outputDir, recompile: false }); - poseidon = await buildPoseidon(); - F = poseidon.F; - babyJub = await buildBabyjub(); - - // Derive auditor Baby Jubjub pubkey: pk = sk · G - const pk = babyJub.mulPointEscalar(babyJub.Base8, TEST_AUDITOR_SK); - auditorPkX = BigInt(babyJub.F.toString(pk[0])); - auditorPkY = BigInt(babyJub.F.toString(pk[1])); - }); - - // ── Helpers ───────────────────────────────────────────────────────────── - - function commitment(value: bigint, assetId: bigint, ownerPk: bigint, blinding: bigint): string { - return F.toString(poseidon([value, assetId, ownerPk, blinding])); - } - - function ownerHash(ownerPk: bigint): bigint { - return BigInt(F.toString(poseidon([ownerPk]))); - } - - /** Build a Poseidon keystream from a Baby Jubjub point. */ - function keystream(sharedX: bigint, sharedY: bigint): [bigint, bigint, bigint] { - const k0 = BigInt(F.toString(poseidon([sharedX, sharedY, 0n]))); - const k1 = BigInt(F.toString(poseidon([sharedX, sharedY, 1n]))); - const k2 = BigInt(F.toString(poseidon([sharedX, sharedY, 2n]))); - return [k0, k1, k2]; - } - - /** Decrypt field: (enc - k + P) mod P */ - function fieldSub(enc: bigint, k: bigint): bigint { - return (enc - k + BN254_P) % BN254_P; - } - - /** Extract the 5 public outputs from the circuit witness. */ - async function runCircuit(input: CircuitInput): Promise { - const witness = await circuit.calculateWitness(input); - await circuit.checkConstraints(witness); - - // Witness layout: [1, out_0, out_1, out_2, out_3, out_4, pub_in_0, pub_in_1, pub_in_2, ...] - // The 5 outputs (epk_x, epk_y, enc_value, enc_asset_id, enc_owner_hash) come before the inputs - return { - epk_x: BigInt(witness[1].toString()), - epk_y: BigInt(witness[2].toString()), - enc_value: BigInt(witness[3].toString()), - enc_asset_id: BigInt(witness[4].toString()), - enc_owner_hash: BigInt(witness[5].toString()), - }; - } - - function baseInput(overrides: Partial = {}): CircuitInput { - const value = 1_000_000n; - const assetId = 0n; - const ownerPk = 12345678901234567890n; - const blinding = 98765432109876543210n; - return { - commitment: commitment(value, assetId, ownerPk, blinding), - auditor_pk_x: auditorPkX.toString(), - auditor_pk_y: auditorPkY.toString(), - value: value.toString(), - asset_id: assetId.toString(), - owner_pubkey: ownerPk.toString(), - blinding: blinding.toString(), - disclose_value: "0", - disclose_asset_id: "0", - disclose_owner: "0", - r: TEST_R.toString(), - ...overrides, - }; - } - - // ── 1. Commitment verification ─────────────────────────────────────────── - - describe("1. Commitment verification", () => { - it("accepts a valid commitment with all fields hidden", async () => { - await runCircuit(baseInput()); - }); - - it("rejects an incorrect commitment (tampered by +1)", async () => { - const c = BigInt(baseInput().commitment) + 1n; - try { - await circuit.calculateWitness(baseInput({ commitment: c.toString() })); - expect.fail("Expected Assert Failed"); - } catch (e: any) { - expect(e.message).to.include("Assert Failed"); - } - }); - - it("changes commitment when value changes", () => { - const c1 = commitment(1000n, 0n, 111n, 222n); - const c2 = commitment(9999n, 0n, 111n, 222n); - expect(c1).to.not.equal(c2); - }); - - it("changes commitment when asset_id changes", () => { - const c1 = commitment(1000n, 0n, 111n, 222n); - const c2 = commitment(1000n, 1n, 111n, 222n); - expect(c1).to.not.equal(c2); - }); - - it("changes commitment when owner_pubkey changes", () => { - const c1 = commitment(1000n, 0n, 111n, 222n); - const c2 = commitment(1000n, 0n, 999n, 222n); - expect(c1).to.not.equal(c2); - }); - - it("changes commitment when blinding changes", () => { - const c1 = commitment(1000n, 0n, 111n, 222n); - const c2 = commitment(1000n, 0n, 111n, 333n); - expect(c1).to.not.equal(c2); - }); - - it("rejects wrong owner_pubkey (can't reconstruct commitment)", async () => { - const c = commitment(1000n, 0n, 777n, 888n); - try { - await circuit.calculateWitness( - baseInput({ - commitment: c, - owner_pubkey: "778", // wrong - }) - ); - expect.fail("Expected Assert Failed"); - } catch (e: any) { - expect(e.message).to.include("Assert Failed"); - } - }); - }); - - // ── 2. ECDH ephemeral key ──────────────────────────────────────────────── - - describe("2. ECDH ephemeral key (epk = r·G)", () => { - it("produces deterministic epk from fixed r", async () => { - const out1 = await runCircuit(baseInput()); - const out2 = await runCircuit(baseInput()); - expect(out1.epk_x).to.equal(out2.epk_x); - expect(out1.epk_y).to.equal(out2.epk_y); - }); - - it("produces different epk when r changes", async () => { - const r2 = (TEST_R + 1n).toString(); - const out1 = await runCircuit(baseInput()); - const out2 = await runCircuit(baseInput({ r: r2 })); - expect(out1.epk_x).to.not.equal(out2.epk_x); - }); - - it("epk matches expected Baby Jubjub scalar mult r·G (off-circuit)", async () => { - const expectedEpk = babyJub.mulPointEscalar(babyJub.Base8, TEST_R); - const out = await runCircuit(baseInput()); - expect(out.epk_x).to.equal(BigInt(babyJub.F.toString(expectedEpk[0]))); - expect(out.epk_y).to.equal(BigInt(babyJub.F.toString(expectedEpk[1]))); - }); - }); - - // ── 3. ECDH symmetry (shared secret) ──────────────────────────────────── - - describe("3. ECDH symmetry — shared secret equals from both sides", () => { - it("r·pk_A == sk_A·epk (off-circuit verification)", async () => { - const out = await runCircuit(baseInput()); - - // Owner's side: shared = r · pk_A - const pkPoint = [ - babyJub.F.e(auditorPkX.toString()), - babyJub.F.e(auditorPkY.toString()), - ]; - const sharedOwner = babyJub.mulPointEscalar(pkPoint, TEST_R); - - // Auditor's side: shared = sk_A · epk - const epkPoint = [babyJub.F.e(out.epk_x.toString()), babyJub.F.e(out.epk_y.toString())]; - const sharedAuditor = babyJub.mulPointEscalar(epkPoint, TEST_AUDITOR_SK); - - expect(babyJub.F.toString(sharedOwner[0])).to.equal( - babyJub.F.toString(sharedAuditor[0]) - ); - expect(babyJub.F.toString(sharedOwner[1])).to.equal( - babyJub.F.toString(sharedAuditor[1]) - ); - }); - }); - - // ── 4. Encryption: ciphertext correctness ─────────────────────────────── - - describe("4. Encryption correctness (enc = plaintext + keystream)", () => { - const VALUE = 500_000n; - const ASSET_ID = 3n; - const OWNER_PK = 77777777777777n; - const BLINDING = 99999999999999n; - - function buildInput(flags: { v: boolean; a: boolean; o: boolean }): CircuitInput { - return { - commitment: commitment(VALUE, ASSET_ID, OWNER_PK, BLINDING), - auditor_pk_x: auditorPkX.toString(), - auditor_pk_y: auditorPkY.toString(), - value: VALUE.toString(), - asset_id: ASSET_ID.toString(), - owner_pubkey: OWNER_PK.toString(), - blinding: BLINDING.toString(), - disclose_value: flags.v ? "1" : "0", - disclose_asset_id: flags.a ? "1" : "0", - disclose_owner: flags.o ? "1" : "0", - r: TEST_R.toString(), - }; - } - - /** Compute shared secret (owner side): r · pk_A */ - function sharedSecret(r: bigint): [bigint, bigint] { - const pkPoint = [ - babyJub.F.e(auditorPkX.toString()), - babyJub.F.e(auditorPkY.toString()), - ]; - const s = babyJub.mulPointEscalar(pkPoint, r); - return [BigInt(babyJub.F.toString(s[0])), BigInt(babyJub.F.toString(s[1]))]; - } - - it("enc_value decrypts to value when disclose_value=1", async () => { - const out = await runCircuit(buildInput({ v: true, a: false, o: false })); - const [sx, sy] = sharedSecret(TEST_R); - const [k0] = keystream(sx, sy); - expect(fieldSub(out.enc_value, k0)).to.equal(VALUE); - }); - - it("enc_value decrypts to 0 when disclose_value=0", async () => { - const out = await runCircuit(buildInput({ v: false, a: false, o: false })); - const [sx, sy] = sharedSecret(TEST_R); - const [k0] = keystream(sx, sy); - expect(fieldSub(out.enc_value, k0)).to.equal(0n); - }); - - it("enc_asset_id decrypts to asset_id when disclose_asset_id=1", async () => { - const out = await runCircuit(buildInput({ v: false, a: true, o: false })); - const [sx, sy] = sharedSecret(TEST_R); - const [, k1] = keystream(sx, sy); - expect(fieldSub(out.enc_asset_id, k1)).to.equal(ASSET_ID); - }); - - it("enc_owner_hash decrypts to Poseidon(owner_pubkey) when disclose_owner=1", async () => { - const out = await runCircuit(buildInput({ v: false, a: false, o: true })); - const [sx, sy] = sharedSecret(TEST_R); - const [, , k2] = keystream(sx, sy); - expect(fieldSub(out.enc_owner_hash, k2)).to.equal(ownerHash(OWNER_PK)); - }); - - it("enc_owner_hash decrypts to 0 when disclose_owner=0", async () => { - const out = await runCircuit(buildInput({ v: false, a: false, o: false })); - const [sx, sy] = sharedSecret(TEST_R); - const [, , k2] = keystream(sx, sy); - expect(fieldSub(out.enc_owner_hash, k2)).to.equal(0n); - }); - - it("all three fields decrypt correctly when all flags=1", async () => { - const out = await runCircuit(buildInput({ v: true, a: true, o: true })); - const [sx, sy] = sharedSecret(TEST_R); - const [k0, k1, k2] = keystream(sx, sy); - expect(fieldSub(out.enc_value, k0)).to.equal(VALUE); - expect(fieldSub(out.enc_asset_id, k1)).to.equal(ASSET_ID); - expect(fieldSub(out.enc_owner_hash, k2)).to.equal(ownerHash(OWNER_PK)); - }); - }); - - // ── 5. Round-trip: different r, same decrypted plaintext ──────────────── - - describe("5. Round-trip with different r", () => { - it("changing r produces different ciphertexts but same plaintext after decrypt", async () => { - const value = 123_456n; - const assetId = 1n; - const ownerPk = 9999999999n; - const blind = 1234567890n; - const c = commitment(value, assetId, ownerPk, blind); - - const r1 = TEST_R; - const r2 = TEST_R + 7919n; // different prime-sized offset - - const input1: CircuitInput = { - commitment: c, - auditor_pk_x: auditorPkX.toString(), - auditor_pk_y: auditorPkY.toString(), - value: value.toString(), - asset_id: assetId.toString(), - owner_pubkey: ownerPk.toString(), - blinding: blind.toString(), - disclose_value: "1", - disclose_asset_id: "1", - disclose_owner: "1", - r: r1.toString(), - }; - const input2 = { ...input1, r: r2.toString() }; - - const out1 = await runCircuit(input1); - const out2 = await runCircuit(input2); - - // Ciphertexts differ - expect(out1.epk_x).to.not.equal(out2.epk_x); - expect(out1.enc_value).to.not.equal(out2.enc_value); - - // But decrypt to same plaintext - const pkPoint = [ - babyJub.F.e(auditorPkX.toString()), - babyJub.F.e(auditorPkY.toString()), - ]; - const s1 = babyJub.mulPointEscalar(pkPoint, r1); - const s2 = babyJub.mulPointEscalar(pkPoint, r2); - const [k0_1] = keystream( - BigInt(babyJub.F.toString(s1[0])), - BigInt(babyJub.F.toString(s1[1])) - ); - const [k0_2] = keystream( - BigInt(babyJub.F.toString(s2[0])), - BigInt(babyJub.F.toString(s2[1])) - ); - - expect(fieldSub(out1.enc_value, k0_1)).to.equal(value); - expect(fieldSub(out2.enc_value, k0_2)).to.equal(value); - }); - }); - - // ── 6. Disclosure mask: boolean constraints ────────────────────────────── - - describe("6. Disclosure mask boolean constraints", () => { - it("rejects disclose_value=2 (non-boolean)", async () => { - try { - await circuit.calculateWitness(baseInput({ disclose_value: "2" })); - expect.fail("Expected Assert Failed"); - } catch (e: any) { - expect(e.message).to.include("Assert Failed"); - } - }); - - it("rejects disclose_asset_id=255 (non-boolean)", async () => { - try { - await circuit.calculateWitness(baseInput({ disclose_asset_id: "255" })); - expect.fail("Expected Assert Failed"); - } catch (e: any) { - expect(e.message).to.include("Assert Failed"); - } - }); - }); - - // ── 7. Owner hash: Poseidon, never raw pubkey ──────────────────────────── - - describe("7. Owner revealed as Poseidon hash, not raw pubkey", () => { - it("enc_owner_hash decrypts to Poseidon(owner_pubkey), not owner_pubkey itself", async () => { - const ownerPk = 42424242424242n; - const c = commitment(1000n, 0n, ownerPk, 111n); - const out = await runCircuit({ - commitment: c, - auditor_pk_x: auditorPkX.toString(), - auditor_pk_y: auditorPkY.toString(), - value: "1000", - asset_id: "0", - owner_pubkey: ownerPk.toString(), - blinding: "111", - disclose_value: "0", - disclose_asset_id: "0", - disclose_owner: "1", - r: TEST_R.toString(), - }); - - const pkPoint = [ - babyJub.F.e(auditorPkX.toString()), - babyJub.F.e(auditorPkY.toString()), - ]; - const shared = babyJub.mulPointEscalar(pkPoint, TEST_R); - const [, , k2] = keystream( - BigInt(babyJub.F.toString(shared[0])), - BigInt(babyJub.F.toString(shared[1])) - ); - - const decrypted = fieldSub(out.enc_owner_hash, k2); - expect(decrypted).to.equal(ownerHash(ownerPk)); - expect(decrypted).to.not.equal(ownerPk); // raw pubkey never exposed - }); - }); -}); - -// ─── Legacy tests removed — circuit interface changed with ECDH on-circuit ─── -// Old Phase 2 suite used plaintext public outputs (revealed_value etc.). -// The new interface encrypts all field disclosures. See sections 1-7 above. diff --git a/test/value_proof.test.ts b/test/value_proof.test.ts new file mode 100644 index 0000000..8fb815e --- /dev/null +++ b/test/value_proof.test.ts @@ -0,0 +1,275 @@ +import path from "path"; +import fs from "fs"; +import { expect } from "chai"; +import { wasm as wasm_tester } from "circom_tester"; +import { buildPoseidon } from "circomlibjs"; +import type { WasmTester } from "circom_tester"; + +// ─── Constants ──────────────────────────────────────────────────────────────── + +// Deterministic test scalars (well within BN254 scalar field) +const OWNER_PUBKEY = 0xdeadbeef_cafebabe_12345678_90abcdefn; +const BLINDING = 0xfedcba09_87654321_aabbccdd_eeff0011n; +const VALUE = 1_000n; // u64 relay fee amount +const ASSET_ID = 0n; // native asset + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +describe("ValueProof Circuit", function () { + this.timeout(120_000); + + const circuitPath = path.join(__dirname, "..", "circuits", "value_proof.circom"); + const outputDir = path.join(__dirname, "..", "build"); + const precompiledWasm = path.join(outputDir, "value_proof_js", "value_proof.wasm"); + + let circuit: WasmTester; + let poseidon: any; + let F: any; + + // ── Lifecycle ────────────────────────────────────────────────────────────── + + before(async function () { + poseidon = await buildPoseidon(); + F = poseidon.F; + + const recompile = !fs.existsSync(precompiledWasm); + circuit = await wasm_tester(circuitPath, { output: outputDir, recompile }); + }); + + // ── Pure helpers ─────────────────────────────────────────────────────────── + + /** Poseidon(value, asset_id, owner_pubkey, blinding) → commitment (bigint). */ + function computeCommitment( + value: bigint, + assetId: bigint, + ownerPubkey: bigint, + blinding: bigint + ): bigint { + return F.toObject(poseidon([value, assetId, ownerPubkey, blinding])); + } + + /** Poseidon(owner_pubkey) → owner_hash (bigint). */ + function computeOwnerHash(ownerPubkey: bigint): bigint { + return F.toObject(poseidon([ownerPubkey])); + } + + /** Build a valid circuit input for the given parameters. */ + function buildInput( + opts: { + value?: bigint; + assetId?: bigint; + ownerPubkey?: bigint; + blinding?: bigint; + commitment?: bigint; // override commitment (to test mismatches) + } = {} + ) { + const value = opts.value ?? VALUE; + const assetId = opts.assetId ?? ASSET_ID; + const ownerPubkey = opts.ownerPubkey ?? OWNER_PUBKEY; + const blinding = opts.blinding ?? BLINDING; + const commitment = + opts.commitment ?? computeCommitment(value, assetId, ownerPubkey, blinding); + + return { + commitment: commitment.toString(), + value: value.toString(), + asset_id: assetId.toString(), + owner_pubkey: ownerPubkey.toString(), + blinding: blinding.toString(), + }; + } + + // ── Happy-path tests ─────────────────────────────────────────────────────── + + describe("valid inputs", () => { + it("should satisfy all constraints", async () => { + const input = buildInput(); + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + }); + + it("should output owner_hash = Poseidon(owner_pubkey)", async () => { + const input = buildInput(); + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + + // witness[1] is the first output signal — owner_hash + const gotOwnerHash = witness[1]; + const expectedOwnerHash = computeOwnerHash(OWNER_PUBKEY); + + expect(gotOwnerHash.toString()).to.equal(expectedOwnerHash.toString()); + }); + + it("should be deterministic for the same inputs", async () => { + const input = buildInput(); + const witness1 = await circuit.calculateWitness(input); + const witness2 = await circuit.calculateWitness(input); + + expect(witness1[1].toString()).to.equal(witness2[1].toString()); + }); + + it("should work for value = 0 (zero-amount note)", async () => { + const input = buildInput({ value: 0n }); + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + }); + + it("should work for maximum u64 value", async () => { + const maxU64 = (1n << 64n) - 1n; // 18_446_744_073_709_551_615 + const input = buildInput({ value: maxU64 }); + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + }); + + it("should work with non-zero asset_id", async () => { + const input = buildInput({ assetId: 42n }); + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + }); + + it("owner_hash differs when owner_pubkey differs", async () => { + const input1 = buildInput({ ownerPubkey: 111n }); + const input2 = buildInput({ ownerPubkey: 222n }); + + const w1 = await circuit.calculateWitness(input1); + const w2 = await circuit.calculateWitness(input2); + + expect(w1[1].toString()).to.not.equal(w2[1].toString()); + }); + }); + + // ── Constraint-violation tests ───────────────────────────────────────────── + + describe("invalid inputs — constraint violations", () => { + it("should fail when commitment does not match preimage (wrong value)", async () => { + // Commitment built for VALUE=1000 but circuit receives value=10000 + const commitment = computeCommitment(VALUE, ASSET_ID, OWNER_PUBKEY, BLINDING); + const input = buildInput({ value: 10_000n, commitment }); + + try { + await circuit.calculateWitness(input); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + + it("should fail when commitment does not match preimage (wrong asset_id)", async () => { + const commitment = computeCommitment(VALUE, ASSET_ID, OWNER_PUBKEY, BLINDING); + const input = buildInput({ assetId: 99n, commitment }); + + try { + await circuit.calculateWitness(input); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + + it("should fail when commitment does not match preimage (wrong blinding)", async () => { + const commitment = computeCommitment(VALUE, ASSET_ID, OWNER_PUBKEY, BLINDING); + const input = buildInput({ blinding: 0xdeadbeefn, commitment }); + + try { + await circuit.calculateWitness(input); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + + it("should fail when commitment does not match preimage (wrong owner_pubkey)", async () => { + const commitment = computeCommitment(VALUE, ASSET_ID, OWNER_PUBKEY, BLINDING); + const input = buildInput({ ownerPubkey: 0xcafebaben, commitment }); + + try { + await circuit.calculateWitness(input); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + + it("should fail when commitment is zero (trivially invalid)", async () => { + const input = buildInput({ commitment: 0n }); + + try { + await circuit.calculateWitness(input); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + }); + + // ── Inflation-attack test ────────────────────────────────────────────────── + + describe("inflation attack prevention", () => { + it("should reject a commitment built with value=1000 when claiming value=10000", async () => { + // Scenario: relayer has 1000 pending fees. + // Attacker builds commitment = Poseidon(1000, asset_id, pk, r). + // Then tries to call claim_shielded_fees with value=10000 in the + // public signals, hoping to unshield 10000 later. + // The circuit MUST reject this because the commitment was built + // for 1000, not 10000. + + const honestCommitment = computeCommitment(1000n, ASSET_ID, OWNER_PUBKEY, BLINDING); + + const attackInput = { + commitment: honestCommitment.toString(), + value: "10000", // ← inflated claim + asset_id: ASSET_ID.toString(), + owner_pubkey: OWNER_PUBKEY.toString(), + blinding: BLINDING.toString(), + }; + + try { + await circuit.calculateWitness(attackInput); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).to.include("Assert Failed"); + } + }); + + it("a valid commitment must use the exact value in the public signal", async () => { + // Confirm the positive case: honest relayer with matching value passes. + const value = 1000n; + const commitment = computeCommitment(value, ASSET_ID, OWNER_PUBKEY, BLINDING); + + const honestInput = { + commitment: commitment.toString(), + value: value.toString(), + asset_id: ASSET_ID.toString(), + owner_pubkey: OWNER_PUBKEY.toString(), + blinding: BLINDING.toString(), + }; + + const witness = await circuit.calculateWitness(honestInput); + await circuit.checkConstraints(witness); + }); + }); + + // ── owner_hash privacy tests ─────────────────────────────────────────────── + + describe("owner_hash reveals only the hash, not the pubkey", () => { + it("owner_hash is independent of value and asset_id", async () => { + const hash1 = computeOwnerHash(OWNER_PUBKEY); + + // Different value, same owner_pubkey → same owner_hash + const input2 = buildInput({ value: 9999n }); + const w2 = await circuit.calculateWitness(input2); + await circuit.checkConstraints(w2); + + expect(w2[1].toString()).to.equal(hash1.toString()); + }); + + it("different blinding values produce the same owner_hash", async () => { + const w1 = await circuit.calculateWitness(buildInput({ blinding: 111n })); + const w2 = await circuit.calculateWitness(buildInput({ blinding: 222n })); + await circuit.checkConstraints(w1); + await circuit.checkConstraints(w2); + + expect(w1[1].toString()).to.equal(w2[1].toString()); + }); + }); +}); From d637a928b5fba57c0cd41132ab837ffecb105b82 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:38:06 -0400 Subject: [PATCH 04/23] Feat: update build scripts to include value proof circuit and adjust pipeline examples --- scripts/build-all.sh | 14 +++++++------- scripts/build/convert-to-ark.sh | 4 ++-- scripts/build/full-pipeline.sh | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/build-all.sh b/scripts/build-all.sh index b8cc25a..46fb4a9 100755 --- a/scripts/build-all.sh +++ b/scripts/build-all.sh @@ -22,16 +22,16 @@ echo "" # Check if node_modules exists if [ ! -d "node_modules" ]; then - echo -e "${BLUE}[Step 1/4]${NC} Installing dependencies..." + echo -e "${BLUE}[Step 1/5]${NC} Installing dependencies..." pnpm install echo "" else - echo -e "${GREEN}[Step 1/4]${NC} Dependencies already installed ✓" + echo -e "${GREEN}[Step 1/5]${NC} Dependencies already installed ✓" echo "" fi # Build all circuits -CIRCUITS=("disclosure" "transfer" "unshield" "private_link") +CIRCUITS=("transfer" "unshield" "private_link" "value_proof") for i in "${!CIRCUITS[@]}"; do CIRCUIT="${CIRCUITS[$i]}" @@ -50,10 +50,10 @@ echo -e "${GREEN}═════════════════════ echo "" echo -e "${BLUE}Generated Artifacts:${NC}" echo "" -echo -e "${YELLOW}Disclosure Circuit:${NC}" -echo -e " ${YELLOW}•${NC} build/disclosure_js/disclosure.wasm" -echo -e " ${YELLOW}•${NC} keys/disclosure_pk.zkey" -echo -e " ${YELLOW}•${NC} build/verification_key_disclosure.json" +echo -e "${YELLOW}Value Proof Circuit:${NC}" +echo -e " ${YELLOW}•${NC} build/value_proof_js/value_proof.wasm" +echo -e " ${YELLOW}•${NC} keys/value_proof_pk.zkey" +echo -e " ${YELLOW}•${NC} build/verification_key_value_proof.json" echo "" echo -e "${YELLOW}Transfer Circuit:${NC}" echo -e " ${YELLOW}•${NC} build/transfer_js/transfer.wasm" diff --git a/scripts/build/convert-to-ark.sh b/scripts/build/convert-to-ark.sh index 916c561..042e34e 100755 --- a/scripts/build/convert-to-ark.sh +++ b/scripts/build/convert-to-ark.sh @@ -19,8 +19,8 @@ echo -e "${BLUE} Convert .zkey to .ark Format (Arkworks)${NC}" echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" echo "" -# Get circuit name from command line or default to "disclosure" -CIRCUIT_NAME="${1:-disclosure}" +# Get circuit name from command line or default to "value_proof" +CIRCUIT_NAME="${1:-value_proof}" KEYS_DIR="$PROJECT_DIR/keys" ZKEY_FILE="$KEYS_DIR/${CIRCUIT_NAME}_pk.zkey" diff --git a/scripts/build/full-pipeline.sh b/scripts/build/full-pipeline.sh index 9024f74..da020f9 100755 --- a/scripts/build/full-pipeline.sh +++ b/scripts/build/full-pipeline.sh @@ -1,7 +1,7 @@ #!/bin/bash # Full pipeline: compile circuit → setup # Usage: bash scripts/build/full-pipeline.sh -# Example: bash scripts/build/full-pipeline.sh disclosure +# Example: bash scripts/build/full-pipeline.sh value_proof set -e @@ -9,7 +9,7 @@ CIRCUIT=$1 if [ -z "$CIRCUIT" ]; then echo "Usage: $0 " - echo "Example: $0 disclosure" + echo "Example: $0 value_proof" exit 1 fi From ff76fdf77e289fb330c474b6c294e1c7ade84e8c Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:38:17 -0400 Subject: [PATCH 05/23] Feat: update manifest to include value proof circuit and its artifacts --- manifest.json | 90 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/manifest.json b/manifest.json index d7ceec0..b2b480b 100644 --- a/manifest.json +++ b/manifest.json @@ -2,9 +2,9 @@ "schema_version": "1.0.0", "package_name": "orbinum-circuits", "package_version": "0.8.0", - "generated_at": "2026-05-11T17:10:31.620Z", + "generated_at": "2026-05-14T05:45:55.227Z", "circuits": { - "disclosure": { + "value_proof": { "active_version": 1, "supported_versions": [ 1 @@ -12,31 +12,31 @@ "versions": { "1": { "version": 1, - "vk_hash": "0x59f5134a08498917010ac866216c82b79da07a80098b6826c535d609c4a899b4", + "vk_hash": "0xb12b2f109c57080de1e46f9b579820da209f5145fe3ea22859619a7b280050cd", "artifacts": { "wasm": { - "file": "disclosure.wasm", - "localPath": "build/disclosure_js/disclosure.wasm", - "bytes": 2798462, - "sha256": "0595542a3e19d6dc16b7e2af529fc8ab324e9a2c79989dd931d04622246a9dea" + "file": "value_proof.wasm", + "localPath": "build/value_proof_js/value_proof.wasm", + "bytes": 2218540, + "sha256": "4c90956e63260626f069fbf43dcc08d3220d2b745f6ba10306aec403966e8f37" }, "zkey": { - "file": "disclosure_pk.zkey", - "localPath": "keys/disclosure_pk.zkey", - "bytes": 5017200, - "sha256": "ab4710bdb6b17ca57539c5f071e0c0f20ac4afc73acd4dd3816edf79a7700846" + "file": "value_proof_pk.zkey", + "localPath": "keys/value_proof_pk.zkey", + "bytes": 548348, + "sha256": "93007788c416fef3b7f9f34a0710b2b7b0249f744867a51c3713517f18f0a427" }, "vk_json": { - "file": "verification_key_disclosure.json", - "localPath": "build/verification_key_disclosure.json", - "bytes": 4207, - "sha256": "59f5134a08498917010ac866216c82b79da07a80098b6826c535d609c4a899b4" + "file": "verification_key_value_proof.json", + "localPath": "build/verification_key_value_proof.json", + "bytes": 3474, + "sha256": "b12b2f109c57080de1e46f9b579820da209f5145fe3ea22859619a7b280050cd" }, "r1cs": { - "file": "disclosure.r1cs", - "localPath": "build/disclosure.r1cs", - "bytes": 1740612, - "sha256": "5a61fdb41868c5c9d193683f4ccde4e18ce71dfc7363904a6c2e5864a3bbf0f0" + "file": "value_proof.r1cs", + "localPath": "build/value_proof.r1cs", + "bytes": 157776, + "sha256": "07673eac319aa6fe620a58338d33bbf2f53f99b9e0deb906727c5f82aa790654" } } } @@ -50,7 +50,7 @@ "versions": { "1": { "version": 1, - "vk_hash": "0x16b3ee00be0ed829f4a64356f8643b620b42cecdb1437705478b935311a15a52", + "vk_hash": "0x627637482e243cf51b00206c557cbc23356b5a19955ef97c2f5603d4621f4eab", "artifacts": { "wasm": { "file": "transfer.wasm", @@ -62,13 +62,13 @@ "file": "transfer_pk.zkey", "localPath": "keys/transfer_pk.zkey", "bytes": 17247624, - "sha256": "d32e1df8297500adabc1c461a58ac53061655a8cf27242ea2ec86b6452d9f52b" + "sha256": "2de83aef5fc63824f5ae2c4dc555baf79ae8675107e87fc601409f8c893acf7c" }, "vk_json": { "file": "verification_key_transfer.json", "localPath": "build/verification_key_transfer.json", "bytes": 4022, - "sha256": "16b3ee00be0ed829f4a64356f8643b620b42cecdb1437705478b935311a15a52" + "sha256": "627637482e243cf51b00206c557cbc23356b5a19955ef97c2f5603d4621f4eab" }, "r1cs": { "file": "transfer.r1cs", @@ -88,7 +88,7 @@ "versions": { "1": { "version": 1, - "vk_hash": "0xd1eed232a886b10dde8f04fcef01f8e6ad73cb34c5a5429dbd5a7ae1b5304c7d", + "vk_hash": "0x0c88ef765ae8c6df3ccdf6af02d862665d8e5514a8bf33d319dfd4fc713ae7e0", "artifacts": { "wasm": { "file": "unshield.wasm", @@ -100,13 +100,13 @@ "file": "unshield_pk.zkey", "localPath": "keys/unshield_pk.zkey", "bytes": 8653108, - "sha256": "fb6f27f1eb880fafb51dca7dadb9b3e4f76df3bd685cf3d0716752dc6f13cd6c" + "sha256": "34ef7bbda8b7c34d8e245ed59ef97229f07184f2331df48a6df0eec53efb4e52" }, "vk_json": { "file": "verification_key_unshield.json", "localPath": "build/verification_key_unshield.json", - "bytes": 4029, - "sha256": "d1eed232a886b10dde8f04fcef01f8e6ad73cb34c5a5429dbd5a7ae1b5304c7d" + "bytes": 4027, + "sha256": "0c88ef765ae8c6df3ccdf6af02d862665d8e5514a8bf33d319dfd4fc713ae7e0" }, "r1cs": { "file": "unshield.r1cs", @@ -117,6 +117,44 @@ } } } + }, + "private_link": { + "active_version": 1, + "supported_versions": [ + 1 + ], + "versions": { + "1": { + "version": 1, + "vk_hash": "0xc707265a22af45e6f5414960082ccc9906b7f796e16828088106bfd862d084c6", + "artifacts": { + "wasm": { + "file": "private_link.wasm", + "localPath": "build/private_link_js/private_link.wasm", + "bytes": 1750902, + "sha256": "3190bb18260b294c2dccad6f509867a2fdd756707a3c1d10a483551e18796e08" + }, + "zkey": { + "file": "private_link_pk.zkey", + "localPath": "keys/private_link_pk.zkey", + "bytes": 508588, + "sha256": "ca777ed1bfa71830a17ea13933781999cede7ca025da7645efcdf8fa6251c73e" + }, + "vk_json": { + "file": "verification_key_private_link.json", + "localPath": "build/verification_key_private_link.json", + "bytes": 3108, + "sha256": "c707265a22af45e6f5414960082ccc9906b7f796e16828088106bfd862d084c6" + }, + "r1cs": { + "file": "private_link.r1cs", + "localPath": "build/private_link.r1cs", + "bytes": 138248, + "sha256": "62b38b485b501d0423580c5f217f5cc088d46ead3956097cd893358df7b3fa8c" + } + } + } + } } } } From fd861ced485fa85b428fbdbf6b638f390892aa1c Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:38:28 -0400 Subject: [PATCH 06/23] Feat: update package.json to include value proof circuit scripts and bump version to 0.9.0 --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3a5d24c..4fb02ee 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,26 @@ { "name": "orbinum-circuits", - "version": "0.8.0", + "version": "0.9.0", "description": "Zero-Knowledge circuits for Orbinum privacy blockchain", "scripts": { "compile": "pnpm run compile:transfer", "compile:transfer": "bash scripts/build/compile.sh transfer", - "compile:disclosure": "bash scripts/build/compile.sh disclosure", "compile:unshield": "bash scripts/build/compile.sh unshield", "compile:private-link": "bash scripts/build/compile.sh private_link", + "compile:value-proof": "bash scripts/build/compile.sh value_proof", "setup": "pnpm run setup:transfer", "setup:transfer": "bash scripts/build/setup.sh transfer", - "setup:disclosure": "bash scripts/build/setup.sh disclosure", "setup:unshield": "bash scripts/build/setup.sh unshield", "setup:private-link": "bash scripts/build/setup.sh private_link", - "convert:disclosure": "bash scripts/build/convert-to-ark.sh disclosure", + "setup:value-proof": "bash scripts/build/setup.sh value_proof", "convert:transfer": "bash scripts/build/convert-to-ark.sh transfer", "convert:unshield": "bash scripts/build/convert-to-ark.sh unshield", "convert:private-link": "bash scripts/build/convert-to-ark.sh private_link", - "full-build:disclosure": "bash scripts/build/full-pipeline.sh disclosure", + "convert:value-proof": "bash scripts/build/convert-to-ark.sh value_proof", "full-build:transfer": "bash scripts/build/full-pipeline.sh transfer", "full-build:unshield": "bash scripts/build/full-pipeline.sh unshield", "full-build:private-link": "bash scripts/build/full-pipeline.sh private_link", + "full-build:value-proof": "bash scripts/build/full-pipeline.sh value_proof", "build-all": "bash scripts/build-all.sh", "manifest": "ts-node scripts/utils/generate-manifest.ts", "test": "mocha --require ts-node/register 'test/**/*.test.ts' --timeout 100000", From 0b343a8161095945fb43d402ac8bb529e4fcefe6 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:38:59 -0400 Subject: [PATCH 07/23] Feat: update CHANGELOG.md to document value proof circuit addition, removal of disclosure circuit, and related changes --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c9cf70..214c3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to Orbinum Circuits will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.0] - 2026-05-14 + +### Added + +- **`value_proof.circom`**: new circuit that lets a relayer prove a note commitment encodes exactly the declared relay fee amount. Replaces `disclosure.circom` as the note-introspection circuit. Used by `pallet-shielded-pool::claim_shielded_fees`. +- **Public signals layout (76 bytes)**: `commitment[0..32] | value[32..40] | asset_id[40..44] | owner_hash[44..76]`. +- **Public inputs**: `commitment` (Field), `value` (u64 LE), `asset_id` (u32 LE). +- **Public output**: `owner_hash = Poseidon(owner_pubkey)` — exposes the owner hash for off-chain audit without revealing the key. +- **Private inputs**: `owner_pubkey`, `blinding`. +- **2 constraints**: commitment verification (`Poseidon(value, asset_id, owner_pubkey, blinding) == commitment`) and owner hash. +- **`CircuitId::VALUE_PROOF = 6`** added to `pallet-zk-verifier` and `primitives/zk-verifier`. +- **`verify_value_proof()`** on the `ZkVerifierPort` trait with its implementation. +- **`test/value_proof.test.ts`**: 16 tests — happy path (6), constraint violations (5), inflation attack (2), owner_hash privacy (2). +- **`docs/circuits/value_proof.md`**: full documentation covering purpose, signals, constraints, comparison table, usage example, and inflation attack analysis. +- **`value_proof` scripts in `package.json` and `scripts/build-all.sh`**: `compile:value-proof`, `setup:value-proof`, `convert:value-proof`, `full-build:value-proof`. +- **`manifest.json`**: `"value_proof"` block with sha256 pending trusted setup. + +### Removed + +- **`disclosure.circom`**: removed. The on-circuit ECDH Baby Jubjub model is incompatible with the Zcash off-chain viewing-key flow adopted in the `ZCASH_DISCLOSURE_MIGRATION`. Build artifacts (`build/disclosure_js/`, `keys/disclosure_*`) should be deleted from the build environment. +- **`docs/circuits/disclosure.md`**: replaced by `docs/circuits/value_proof.md`. +- **`scripts/generators/`**: entire directory removed (`generate_input.ts`, `generate_unshield_and_private_link_input.js`). +- **`disclosure` scripts from `package.json`**: `compile:disclosure`, `setup:disclosure`, `convert:disclosure`, `full-build:disclosure`. +- **`"disclosure"` block from `manifest.json`**. + +### Changed + +- **`build-all.sh`**: replaced `"disclosure"` with `"value_proof"` in the `CIRCUITS` array. +- **`config/circuits.config.json`**: removed `disclosure` block; added `value_proof` block with `circuitId: 6`, correct signal counts (3 public inputs, 1 public output, 2 private inputs, ~300 constraints), `encryptionScheme: "none"`, and `publicSignalsLayout`. Performance target updated accordingly. +- **`npm/index.js`**: `CIRCUITS` array and JSDoc — `"disclosure"` → `"value_proof"`. +- **`npm/index.d.ts`**: `CircuitType` union and `getCircuitPaths` parameter — `"disclosure"` → `"value_proof"`. +- **`npm/README.md`**: circuit list, type comments, "Available Circuits" section (Disclosure → Value Proof with correct description), and `generateProof` example updated. +- **`scripts/build/full-pipeline.sh`**: usage comment and inline example — `disclosure` → `value_proof`. +- **`scripts/build/convert-to-ark.sh`**: default circuit name — `disclosure` → `value_proof`. +- **Docs updated**: `docs/README.md`, `docs/ARCHITECTURE.md`, `docs/guides/arkworks-integration.md`, `docs/guides/quick-start.md`, `docs/circuits/note.md`, `docs/circuits/poseidon-wrapper.md` — all references to `disclosure` removed; constraint count table updated with `value_proof`. + +### Security + +- **Inflation attack prevented**: without `value_proof`, a malicious relayer could insert a commitment built with `value=10000` while claiming only `fee=1000`, then `unshield` that commitment to drain other users' funds. The circuit enforces `commitment == NoteCommitment(declared_value, ...)`. +- **`claim_relay_fees_to_evm` removed from `pallet-shielded-pool`**: that extrinsic exposed relayer funds publicly (no ZK proof), creating relayer↔funds linkability. The only valid path is now `claim_shielded_fees` with a ZK proof. + ## [0.8.0] - 2026-05-11 ### Added From b7430b2d877e42566e2dd7981b6434b372561fbd Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:08 -0400 Subject: [PATCH 08/23] Feat: update README.md to reflect changes for value proof circuit compilation and usage --- README.md | 140 ++++++++++++++++++------------------------------------ 1 file changed, 47 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 2dde069..fee862a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ pnpm run build-all This automatically: - Installs dependencies -- Compiles circuits (disclosure.circom → R1CS + WASM) +- Compiles circuits (value_proof.circom → R1CS + WASM) - Downloads Powers of Tau (72MB, one-time) - Generates cryptographic keys (proving + verifying keys) - Converts to compatible formats @@ -59,9 +59,9 @@ This creates `manifest.json` at repo root with: **Output:** -- `build/disclosure_js/disclosure.wasm` (2.1MB) - Witness calculator -- `keys/disclosure_pk.zkey` (689KB) - Proving key -- `build/verification_key_disclosure.json` (3.4KB) - Verifying key +- `build/value_proof_js/value_proof.wasm` (<1MB) - Witness calculator +- `keys/value_proof_pk.zkey` (<1MB) - Proving key +- `build/verification_key_value_proof.json` (3.4KB) - Verifying key ## Using with Rust/Substrate @@ -89,7 +89,7 @@ use ark_serialize::CanonicalDeserialize; use std::fs::File; // Load .ark file (fast) -let mut ark_file = File::open("keys/disclosure_pk.ark")?; +let mut ark_file = File::open("keys/transfer_pk.ark")?; let proving_key = ProvingKey::::deserialize_compressed(&mut ark_file)?; // Generate proof @@ -118,14 +118,14 @@ use ark_groth16::Groth16; use std::fs::File; // Read .zkey file directly -let mut zkey_file = File::open("keys/disclosure_pk.zkey")?; +let mut zkey_file = File::open("keys/transfer_pk.zkey")?; let (proving_key, matrices) = read_zkey(&mut zkey_file)?; // Configure circuit with WASM let cfg = CircomConfig::::new( - "build/disclosure_js/disclosure.wasm", - "build/disclosure.r1cs" -)?; + "build/transfer_js/transfer.wasm", + "build/transfer.r1cs" +)?;; // Build circuit with inputs let mut builder = CircomBuilder::new(cfg); @@ -145,11 +145,11 @@ If you need to generate the `.ark` file yourself: ```bash # Using the Rust script cargo +nightly -Zscript scripts/build/convert-to-ark.rs \ - keys/disclosure_pk.zkey \ - keys/disclosure_pk.ark + keys/value_proof_pk.zkey \ + keys/value_proof_pk.ark # Or via pnpm -pnpm run convert:disclosure +pnpm run convert:value-proof ``` ### Download Release Artifacts @@ -158,14 +158,14 @@ Get pre-built circuits from [GitHub Releases](../../releases): ```bash # Download latest release -wget https://github.com/orb-labs/circuits/releases/latest/download/disclosure-circuit-v*.tar.gz +wget https://github.com/orb-labs/circuits/releases/latest/download/orbinum-circuits-v*.tar.gz # Extract files -tar -xzf disclosure-circuit-v*.tar.gz +tar -xzf orbinum-circuits-v*.tar.gz # Use in your Rust project -cp keys/disclosure_pk.zkey /path/to/your/rust/project/ -cp build/disclosure_js/disclosure.wasm /path/to/your/rust/project/ +cp value_proof_pk.zkey /path/to/your/rust/project/ +cp value_proof.wasm /path/to/your/rust/project/ ``` ## Testing @@ -178,47 +178,38 @@ pnpm test **Test Suites:** -- `disclosure.test.ts` - Selective disclosure circuit +- `value_proof.test.ts` - Relay fee value proof (16 tests) - `transfer.test.ts` - Private transfer logic - `unshield.test.ts` - Multi-asset support - `merkle_tree.test.ts` - Merkle proof verification - `note.test.ts` - Note commitment schemes - `poseidon_*.test.ts` - Hash function compatibility -**Expected:** 135 tests passing in ~45 seconds - ### Run Specific Test ```bash -pnpm test -- --grep "disclosure" +pnpm test -- --grep "value_proof" ``` ## Benchmarks ### Prerequisites -Generate test inputs first: +Generate test inputs first (transfer circuit): ```bash -pnpm run gen-input:disclosure +pnpm run gen-input:transfer ``` -This creates 4 test scenarios: - -- `reveal_nothing` - Full privacy -- `reveal_value_only` - Amount visible -- `reveal_value_and_asset` - Amount + asset type visible -- `reveal_all` - Complete disclosure - ### Run Benchmarks ```bash -# Disclosure circuit -pnpm run bench:disclosure - -# Transfer circuit (coming soon) +# Transfer circuit pnpm run bench:transfer +# Unshield circuit +pnpm run bench:unshield + # All circuits pnpm run bench ``` @@ -248,26 +239,6 @@ pnpm run bench Complete automated workflows from compilation to proof generation. -### Disclosure Circuit - -```bash -pnpm run e2e:disclosure -``` - -**What it does:** - -1. Compiles circuit -2. Sets up keys -3. Generates test inputs (4 scenarios) -4. Creates proofs for all scenarios -5. Verifies all proofs - -**Generated artifacts:** - -- 4 input files: `build/disclosure_input_*.json` -- 4 proof files: `build/proof_disclosure_*.json` -- 4 public signals: `build/public_disclosure_*.json` - ### Transfer Circuit ```bash @@ -290,16 +261,16 @@ pnpm run build-all ```bash # Step 1: Compile circuit -pnpm run compile:disclosure +pnpm run compile:value-proof # Step 2: Generate keys (requires compilation) -pnpm run setup:disclosure +pnpm run setup:value-proof # Step 3: Convert to compatible format (optional) -pnpm run convert:discord +pnpm run convert:value-proof # Or run all steps together -pnpm run full-build:disclosure +pnpm run full-build:value-proof ``` ### Generate WASM for Rust (Witness Calculator) @@ -316,19 +287,19 @@ The `fp-encrypted-memo` primitive can use WASM to calculate the complete circuit **From circuits/circuits/ directory:** ```bash -# Compile disclosure.circom to WASM -circom disclosure.circom --wasm --output ../build/ +# Compile value_proof.circom to WASM +circom value_proof.circom --wasm --output ../build/ ``` **Generated file:** -- `build/disclosure_js/disclosure.wasm` (~2.1MB) +- `build/value_proof_js/value_proof.wasm` (<1MB) **Usage in Rust:** ```rust // With feature flag: wasm-witness -let wasm_bytes = std::fs::read("circuits/build/disclosure_js/disclosure.wasm")?; +let wasm_bytes = std::fs::read("circuits/build/value_proof_js/value_proof.wasm")?;; let witness = calculate_witness_wasm(&wasm_bytes, &inputs, &signals)?; ``` @@ -337,9 +308,6 @@ let witness = calculate_witness_wasm(&wasm_bytes, &inputs, &signals)?; ### Generate Test Inputs ```bash -# Disclosure circuit (4 scenarios) -pnpm run gen-input:disclosure - # Transfer circuit pnpm run gen-input:transfer ``` @@ -347,9 +315,6 @@ pnpm run gen-input:transfer ### Generate Proofs ```bash -# Disclosure proofs -pnpm run prove:disclosure - # Transfer proofs pnpm run prove:transfer ``` @@ -401,27 +366,25 @@ pnpm run prove:transfer - Asset ID binding: `note_asset_id === asset_id`; change commitment pinned to same asset - `recipient` is a public signal (validated non-zero in the pallet) -### Disclosure Circuit — `circuits/disclosure.circom` +### Value Proof Circuit — `circuits/value_proof.circom` -**Purpose:** Selective on-chain disclosure of note fields to an auditor, ECDH-encrypted on-circuit over Baby Jubjub +**Purpose:** Proves a note commitment encodes exactly the declared relay fee amount before the runtime inserts it into the Merkle tree. Used by `pallet-shielded-pool::claim_shielded_fees`. **Statistics:** -- Constraints: 9,411 (7,557 non-linear + 1,854 linear) -- Private inputs: 8 (`value`, `asset_id`, `owner_pubkey`, `blinding`, `disclose_value`, `disclose_asset_id`, `disclose_owner`, `r`) -- Public inputs: 3 (`commitment`, `auditor_pk_x`, `auditor_pk_y`) -- Public outputs: 5 (`epk_x`, `epk_y`, `enc_value`, `enc_asset_id`, `enc_owner_hash`) +- Constraints: ~300 +- Private inputs: 2 (`owner_pubkey`, `blinding`) +- Public inputs: 3 (`commitment`, `value`, `asset_id`) +- Public outputs: 1 (`owner_hash`) +- CircuitId: `6` (`CircuitId::VALUE_PROOF`) **Features:** - Commitment preimage proof: `commitment === Poseidon(value, asset_id, owner_pubkey, blinding)` -- Selective field encryption controlled by boolean masks (`disclose_*`); masked fields encrypt as `0` -- ECDH key exchange on-circuit: `epk = r·G` (EscalarMulFix), `shared = r·pk_A` (EscalarMulAny) -- Poseidon keystream: `k_i = Poseidon(shared.x, shared.y, i)` for each field (i = 0, 1, 2) -- Ciphertext: `enc_field = masked_field + k_i` (field addition mod BN254 scalar field) -- Owner disclosed as `Poseidon(owner_pubkey)` instead of raw pubkey (privacy-preserving) -- Boolean mask constraints: `disclose_* * (disclose_* - 1) === 0` -- Ephemeral scalar `r` range-checked via `Num2Bits(253)` +- Owner hash: `owner_hash = Poseidon(owner_pubkey)` — reveals owner identity hash for audit without exposing the raw key +- No Merkle proof, no spending key, no nullifier — proves note formation only +- Prevents inflation attacks: relayer cannot claim `value=10000` if commitment was built with `value=1000` +- Public signals layout (76 bytes): `commitment[0..32] | value[32..40] | asset_id[40..44] | owner_hash[44..76]` ### Private Link Circuit — `circuits/private_link.circom` @@ -465,24 +428,23 @@ circuits/ ├── circuits/ # Circom source files │ ├── transfer.circom # 2-in/2-out private transfer (33,687 constraints) │ ├── unshield.circom # Private → public withdrawal (16,903 constraints) -│ ├── disclosure.circom # Selective field disclosure, ECDH-encrypted (9,411 constraints) +│ ├── value_proof.circom # Relay fee value proof, no Merkle/nullifier (~300 constraints) │ ├── private_link.circom # Cross-chain identity link (487 constraints) │ ├── note.circom # NoteCommitment + Nullifier templates │ ├── merkle_tree.circom # MerkleTreeVerifier template │ └── poseidon_wrapper.circom ├── build/ # Compiled artifacts │ ├── transfer_js/transfer.wasm -│ ├── disclosure_js/disclosure.wasm +│ ├── value_proof_js/value_proof.wasm │ └── verification_key_*.json ├── keys/ # Cryptographic keys │ ├── *_pk.zkey # snarkjs proving keys │ └── *_pk.ark # arkworks proving keys (serialized) ├── test/ # Test suites (135 tests) ├── benches/ # Performance benchmarks -├── scripts/ # Build and generation scripts +├── scripts/ # Build scripts │ ├── build/ # Compilation scripts -│ ├── generators/ # Input/proof generators -│ └── e2e-*.ts # End-to-end workflows +│ └── utils/ # Manifest and lint utilities └── package.json ``` @@ -497,14 +459,6 @@ All requirements are checked automatically by build scripts. ## Troubleshooting -### "Missing disclosure input files" - -Run input generator first: - -```bash -pnpm run gen-input:disclosure -``` - ### "Powers of Tau download failed" Check internet connection. The script will retry with fallback URLs automatically. From a18c5d3c04a66f99fc193d471e8d636db724ae3c Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:19 -0400 Subject: [PATCH 09/23] Feat: update PULL_REQUEST_TEMPLATE to reflect Value Proof circuit instead of Disclosure --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8ef99fb..9c211f3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -39,7 +39,7 @@ Fixes #(issue number) **Affected Circuits:** -- [ ] Disclosure +- [ ] Value Proof - [ ] Transfer - [ ] Unshield - [ ] Core components From a9d3c6e525b0e401334cf5dc909a73846734390a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:24 -0400 Subject: [PATCH 10/23] Feat: update bug report template to replace Disclosure with Value Proof circuit --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c06a1e2..d86356e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -35,7 +35,7 @@ What actually happened. Which circuit is affected? -- [ ] Disclosure +- [ ] Value Proof - [ ] Transfer - [ ] Unshield - [ ] Other: \***\*\_\_\_\*\*** From 98ce8c4743e0e9fe8575a2b7046d66b6a6390632 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:30 -0400 Subject: [PATCH 11/23] Feat: update feature request template to replace Disclosure with Value Proof circuit --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b0adde4..37808e3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -26,7 +26,7 @@ What alternative solutions or features have you considered? Which circuits would this affect? -- [ ] Disclosure +- [ ] Value Proof - [ ] Transfer - [ ] Unshield - [ ] New circuit From 1ef44c08af1c99264af7d5178b7adb098b84b366 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:38 -0400 Subject: [PATCH 12/23] Feat: update release workflow to replace Disclosure with Value Proof circuit --- .github/workflows/release.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0187346..17bf5ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,8 +90,8 @@ jobs: rustup component add rust-src --toolchain nightly cargo +nightly -Zscript scripts/build/convert-to-ark.rs \ - keys/disclosure_pk.zkey \ - keys/disclosure_pk.ark + keys/value_proof_pk.zkey \ + keys/value_proof_pk.ark cargo +nightly -Zscript scripts/build/convert-to-ark.rs \ keys/transfer_pk.zkey \ @@ -115,9 +115,9 @@ jobs: sha256sum manifest.json > release/checksums.txt cd build - sha256sum disclosure.r1cs >> ../release/checksums.txt - sha256sum disclosure_js/disclosure.wasm >> ../release/checksums.txt - sha256sum verification_key_disclosure.json >> ../release/checksums.txt + sha256sum value_proof.r1cs >> ../release/checksums.txt + sha256sum value_proof_js/value_proof.wasm >> ../release/checksums.txt + sha256sum verification_key_value_proof.json >> ../release/checksums.txt sha256sum transfer.r1cs >> ../release/checksums.txt sha256sum transfer_js/transfer.wasm >> ../release/checksums.txt sha256sum verification_key_transfer.json >> ../release/checksums.txt @@ -129,8 +129,8 @@ jobs: sha256sum verification_key_private_link.json >> ../release/checksums.txt cd ../keys - sha256sum disclosure_pk.zkey >> ../release/checksums.txt - sha256sum disclosure_pk.ark >> ../release/checksums.txt + sha256sum value_proof_pk.zkey >> ../release/checksums.txt + sha256sum value_proof_pk.ark >> ../release/checksums.txt sha256sum transfer_pk.zkey >> ../release/checksums.txt sha256sum transfer_pk.ark >> ../release/checksums.txt sha256sum unshield_pk.zkey >> ../release/checksums.txt @@ -147,11 +147,11 @@ jobs: mkdir -p release/snarkjs-temp mkdir -p release/verification-keys-temp - cp build/disclosure_js/disclosure.wasm release/arkworks-temp/ + cp build/value_proof_js/value_proof.wasm release/arkworks-temp/ cp build/transfer_js/transfer.wasm release/arkworks-temp/ cp build/unshield_js/unshield.wasm release/arkworks-temp/ cp build/private_link_js/private_link.wasm release/arkworks-temp/ - cp keys/disclosure_pk.ark release/arkworks-temp/ + cp keys/value_proof_pk.ark release/arkworks-temp/ cp keys/transfer_pk.ark release/arkworks-temp/ cp keys/unshield_pk.ark release/arkworks-temp/ cp keys/private_link_pk.ark release/arkworks-temp/ @@ -160,7 +160,7 @@ jobs: tar -czf ../orbinum-circuits-${VERSION}.tar.gz * cd ../.. - cp keys/disclosure_pk.zkey release/snarkjs-temp/ + cp keys/value_proof_pk.zkey release/snarkjs-temp/ cp keys/transfer_pk.zkey release/snarkjs-temp/ cp keys/unshield_pk.zkey release/snarkjs-temp/ cp keys/private_link_pk.zkey release/snarkjs-temp/ @@ -169,7 +169,7 @@ jobs: tar -czf ../orbinum-circuits-snarkjs-${VERSION}.tar.gz * cd ../.. - cp build/verification_key_disclosure.json release/verification-keys-temp/ + cp build/verification_key_value_proof.json release/verification-keys-temp/ cp build/verification_key_transfer.json release/verification-keys-temp/ cp build/verification_key_unshield.json release/verification-keys-temp/ cp build/verification_key_private_link.json release/verification-keys-temp/ @@ -237,27 +237,27 @@ jobs: cp manifest.json pkg/ - cp build/disclosure.r1cs pkg/ + cp build/value_proof.r1cs pkg/ cp build/transfer.r1cs pkg/ cp build/unshield.r1cs pkg/ cp build/private_link.r1cs pkg/ - cp build/disclosure_js/disclosure.wasm pkg/ + cp build/value_proof_js/value_proof.wasm pkg/ cp build/transfer_js/transfer.wasm pkg/ cp build/unshield_js/unshield.wasm pkg/ cp build/private_link_js/private_link.wasm pkg/ - cp keys/disclosure_pk.zkey pkg/ + cp keys/value_proof_pk.zkey pkg/ cp keys/transfer_pk.zkey pkg/ cp keys/unshield_pk.zkey pkg/ cp keys/private_link_pk.zkey pkg/ - cp keys/disclosure_pk.ark pkg/ + cp keys/value_proof_pk.ark pkg/ cp keys/transfer_pk.ark pkg/ cp keys/unshield_pk.ark pkg/ cp keys/private_link_pk.ark pkg/ - cp build/verification_key_disclosure.json pkg/ + cp build/verification_key_value_proof.json pkg/ cp build/verification_key_transfer.json pkg/ cp build/verification_key_unshield.json pkg/ cp build/verification_key_private_link.json pkg/ From 2d626bf81338276bc90882c6c4bec7a20a10e3b5 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:44 -0400 Subject: [PATCH 13/23] Feat: replace Disclosure circuit with Value Proof circuit in configuration --- config/circuits.config.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config/circuits.config.json b/config/circuits.config.json index a2d0db4..abb5e4b 100644 --- a/config/circuits.config.json +++ b/config/circuits.config.json @@ -1,15 +1,15 @@ { "circuits": { - "disclosure": { - "name": "Selective Disclosure", - "file": "disclosure.circom", - "merkleDepth": 20, - "maxAssets": 8, - "constraints": 9411, + "value_proof": { + "name": "Value Proof", + "file": "value_proof.circom", + "circuitId": 6, + "constraints": 300, "publicInputs": 3, - "publicOutputs": 5, - "privateInputs": 8, - "encryptionScheme": "ecdh-babyjubjub-poseidon" + "publicOutputs": 1, + "privateInputs": 2, + "encryptionScheme": "none", + "publicSignalsLayout": "commitment[0..32] | value[32..40] | asset_id[40..44] | owner_hash[44..76]" }, "transfer": { "name": "Private Transfer", @@ -60,10 +60,10 @@ }, "performance": { "targets": { - "disclosure": { - "proofTime": "1500ms", + "value_proof": { + "proofTime": "50ms", "verifyTime": "5ms", - "constraints": 9411 + "constraints": 300 }, "transfer": { "proofTime": "2000ms", From b42b6128cc3f03034194500d50c292699b83ddb3 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:50 -0400 Subject: [PATCH 14/23] Feat: replace Disclosure circuit references with Value Proof circuit in documentation --- docs/ARCHITECTURE.md | 54 ++++++++++---------------------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4080660..4e96d70 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -17,7 +17,7 @@ orbinum-circuits/ │ └── PRE_COMMIT.md # Pre-commit documentation │ ├── circuits/ # Circom circuit definitions (flat) -│ ├── disclosure.circom # Selective disclosure +│ ├── value_proof.circom # Note formation proof (relay-fee claiming) │ ├── merkle_tree.circom # Merkle tree component │ ├── note.circom # Note commitment │ ├── poseidon_wrapper.circom # Poseidon hash wrapper @@ -34,16 +34,7 @@ orbinum-circuits/ │ │ ├── convert-to-ark.rs # Rust script (.zkey → .ark) │ │ ├── extract-vk.rs # Rust script (extract verifying key) │ │ └── generate-metadata.sh -│ ├── generators/ # Input/proof generators -│ │ ├── generate_disclosure_input.ts -│ │ ├── generate_disclosure_proof.ts -│ │ ├── generate_input.ts -│ │ ├── generate_proof.ts -│ │ ├── generate_unshield_and_private_link_input.js -│ │ ├── proof_wrapper.ts -│ │ └── eddsa_signer.ts │ ├── e2e/ # End-to-end tests -│ │ ├── e2e-disclosure.ts │ │ └── e2e-transfer.ts │ ├── utils/ # Utilities │ │ ├── check-artifacts.ts @@ -54,7 +45,7 @@ orbinum-circuits/ │ └── README.md │ ├── test/ # Test suite (flat) -│ ├── disclosure.test.ts +│ ├── value_proof.test.ts │ ├── merkle_tree.test.ts │ ├── note.test.ts │ ├── poseidon_compat.test.ts @@ -66,7 +57,6 @@ orbinum-circuits/ │ ├── benches/ # Performance benchmarks │ ├── run-all.bench.ts -│ ├── disclosure.bench.ts │ ├── transfer.bench.ts │ ├── types.ts │ └── utils.ts @@ -92,7 +82,7 @@ orbinum-circuits/ ├── docs/ # Documentation │ ├── ARCHITECTURE.md │ ├── circuits/ # Circuit specifications -│ │ ├── disclosure.md +│ │ ├── value_proof.md │ │ ├── merkle-tree.md │ │ ├── note.md │ │ ├── poseidon-wrapper.md @@ -126,7 +116,7 @@ orbinum-circuits/ **Organization** (flat — all `.circom` files at root level): - `merkle_tree.circom`, `note.circom`, `poseidon_wrapper.circom`: Reusable components -- `disclosure.circom`, `transfer.circom`, `unshield.circom`, `private_link.circom`: Application circuits +- `value_proof.circom`, `transfer.circom`, `unshield.circom`, `private_link.circom`: Application circuits `transfer.circom` implements a 2-in/2-out scheme with **dummy input support**: when a user has only one note, the second input slot carries `value = 0` and bypasses Merkle membership and nullifier derivation (Zcash Sapling technique). Ownership is proven via `BabyPbk(spending_key)` — no EdDSA signatures required. The dummy nullifier is forced to zero by the circuit (Constraint 9). The pallet rejects transactions where both nullifiers are zero (anti-spam). @@ -162,7 +152,7 @@ orbinum-circuits/ **Test files** (flat structure): -- `disclosure.test.ts`, `transfer.test.ts`, `unshield.test.ts`, `private_link.test.ts`: Application circuit tests +- `value_proof.test.ts`, `transfer.test.ts`, `unshield.test.ts`, `private_link.test.ts`: Application circuit tests - `merkle_tree.test.ts`, `note.test.ts`, `poseidon_wrapper.test.ts`, `poseidon_compat.test.ts`: Component tests - `test-utils.ts`: Shared test helpers @@ -181,7 +171,7 @@ orbinum-circuits/ **Files**: - `run-all.bench.ts`: Runs all benchmarks sequentially -- `disclosure.bench.ts`, `transfer.bench.ts`: Per-circuit benchmarks +- `transfer.bench.ts`: Per-circuit benchmarks - `types.ts`, `utils.ts`: Shared benchmark helpers **Metrics**: @@ -192,20 +182,7 @@ orbinum-circuits/ - Memory usage - Throughput (operations/second) -### 5. **Code Generators** (`scripts/generators/`) - -**Purpose**: Generate inputs and proofs programmatically - -**Generators**: - -- `generate_input.ts`: Create valid transfer circuit inputs -- `generate_disclosure_input.ts`: Create disclosure circuit inputs -- `generate_proof.ts` / `generate_disclosure_proof.ts`: Generate ZK proofs -- `generate_unshield_and_private_link_input.js`: Inputs for unshield + private link -- `proof_wrapper.ts`: Proof serialization/deserialization -- `eddsa_signer.ts`: EdDSA signature helper (legacy; ownership is now proven via `BabyPbk` in `transfer` and `unshield`) - -### 6. **npm Package** (`npm/`) +### 5. **npm Package** (`npm/`) **Purpose**: Distributable npm package template for `@orbinum/circuits` @@ -220,7 +197,7 @@ orbinum-circuits/ **Structure**: - **ARCHITECTURE.md**: System design and component interactions (this file) -- **circuits/**: Specifications for disclosure, transfer, unshield, note, merkle-tree, poseidon-wrapper +- **circuits/**: Specifications for value_proof, transfer, unshield, note, merkle-tree, poseidon-wrapper - **guides/**: Quick start, arkworks integration, pre-push checks ## Data Flow @@ -228,12 +205,7 @@ orbinum-circuits/ ### Proof Generation Flow ``` -1. Input Generation - └─> scripts/generators/generate_input.ts - └─> Validate parameters - └─> Create circuit inputs (JSON) - -2. Witness Calculation +1. Witness Calculation └─> build/*_js/*.wasm └─> Execute circuit logic └─> Generate witness (.wtns) @@ -384,10 +356,8 @@ pnpm run format ```json { - "disclosure": { - "merkleDepth": 20, - "maxAssets": 8, - "constraints": 1584 + "value_proof": { + "constraints": 300 }, "transfer": { "merkleDepth": 20, @@ -425,7 +395,7 @@ pnpm run format | Circuit | Constraints | Proof Time | Verify Time | | ------------ | ----------- | ---------- | ----------- | -| Disclosure | 1,584 | <150ms | <5ms | +| Value Proof | ~300 | <50ms | <5ms | | Transfer | 33,687 | <3s | <15ms | | Unshield | 16,903 | <1s | <15ms | | Private Link | 487 | <100ms | <5ms | From 33c5d3ce838b64ed8aa1a571cca70156d6c27de3 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:39:58 -0400 Subject: [PATCH 15/23] Feat: remove Selective Disclosure circuit documentation in favor of Value Proof circuit --- docs/circuits/disclosure.md | 259 ------------------------------------ 1 file changed, 259 deletions(-) delete mode 100644 docs/circuits/disclosure.md diff --git a/docs/circuits/disclosure.md b/docs/circuits/disclosure.md deleted file mode 100644 index 7fd4d27..0000000 --- a/docs/circuits/disclosure.md +++ /dev/null @@ -1,259 +0,0 @@ -# Selective Disclosure Circuit - -**File**: [`circuits/disclosure.circom`](../../circuits/disclosure.circom) - -## Purpose - -The Selective Disclosure circuit lets a note owner prove ownership and **selectively reveal** specific fields (value, asset ID, or owner) to a designated auditor — without leaking anything to third parties. The revealed data is **encrypted on-circuit** using ECDH over Baby Jubjub + Poseidon, so only the auditor can decrypt. - -## Circuit Statement - -> "I know a note that generates this commitment. I encrypt the chosen fields with the auditor's Baby Jubjub public key using ephemeral scalar r, and produce a verifiable ciphertext." - -## Security Properties - -- **Soundness**: Cannot forge a proof without knowing the actual note data. -- **Privacy**: Only the designated auditor can decrypt the revealed fields (ECDH key agreement). -- **Binding**: Proof is bound to a specific commitment on-chain. -- **Auditor-specific**: Different auditor → different public key → different ephemeral shared secret → auditors cannot decrypt each other's disclosures. -- **Non-malleable r**: `r` is private; changing it produces a different ciphertext — verifier cannot reuse a proof for a different auditor. - -## Public Inputs - -| Signal | Type | Description | -| -------------- | ----- | --------------------------------------------- | -| `commitment` | Field | Note commitment (must exist on-chain) | -| `auditor_pk_x` | Field | Auditor Baby Jubjub public key — x coordinate | -| `auditor_pk_y` | Field | Auditor Baby Jubjub public key — y coordinate | - -## Public Outputs (Ciphertext) - -| Signal | Type | Description | -| ---------------- | ----- | --------------------------------------------------- | -| `epk_x` | Field | Ephemeral public key — x coordinate (`r·G`) | -| `epk_y` | Field | Ephemeral public key — y coordinate (`r·G`) | -| `enc_value` | Field | Encrypted value (`value_or_0 + k₀ mod p`) | -| `enc_asset_id` | Field | Encrypted asset ID (`asset_id_or_0 + k₁ mod p`) | -| `enc_owner_hash` | Field | Encrypted owner hash (`owner_hash_or_0 + k₂ mod p`) | - -## Private Inputs - -| Signal | Type | Description | -| ------------------- | ----- | ------------------------------------------------- | -| `value` | Field | Actual note value | -| `asset_id` | Field | Actual asset ID | -| `owner_pubkey` | Field | Owner's public key | -| `blinding` | Field | Blinding factor for commitment | -| `disclose_value` | bool | 1 = encrypt value, 0 = encrypt 0 | -| `disclose_asset_id` | bool | 1 = encrypt asset_id, 0 = encrypt 0 | -| `disclose_owner` | bool | 1 = encrypt Poseidon(owner_pubkey), 0 = encrypt 0 | -| `r` | Field | Ephemeral scalar (random, BN254 scalar field) | - -## Constraints - -### 1. Commitment Verification - -``` -commitment == Poseidon(value, asset_id, owner_pubkey, blinding) -``` - -### 2. Boolean Disclosure Masks - -```circom -disclose_value * (disclose_value - 1) === 0; -disclose_asset_id * (disclose_asset_id - 1) === 0; -disclose_owner * (disclose_owner - 1) === 0; -``` - -### 3. Selective Field Selection - -``` -plain_value = disclose_value ? value : 0 -plain_asset_id = disclose_asset_id ? asset_id : 0 -plain_owner_hash = disclose_owner ? Poseidon(owner_pubkey) : 0 -``` - -### 4. ECDH Key Agreement (Baby Jubjub) - -``` -epk = r · G (EscalarMulFix, base point G = Base8) -shared = r · pk_A (EscalarMulAny, auditor public key) -``` - -**Base point G (Base8)**: - -``` -Gx = 5299619240641551281634865583518297030282874472190772894086521144482721001553 -Gy = 16950150798460657717958625567821834550301663161624707787222815936182638968203 -``` - -### 5. Poseidon Keystream - -``` -k₀ = Poseidon(shared.x, shared.y, 0) -k₁ = Poseidon(shared.x, shared.y, 1) -k₂ = Poseidon(shared.x, shared.y, 2) -``` - -### 6. Field Encryption (mod p addition) - -``` -enc_value = plain_value + k₀ (mod BN254 prime p) -enc_asset_id = plain_asset_id + k₁ (mod BN254 prime p) -enc_owner_hash = plain_owner_hash + k₂ (mod BN254 prime p) -``` - -## Circuit Parameters - -- **Constraints**: ~9,411 (7,557 non-linear + 1,854 linear) -- **Public Inputs**: 3 -- **Public Outputs**: 5 -- **Private Inputs**: 8 -- **Proving Time**: ~1–2 s (local machine) -- **Verification Time**: ~5 ms - -## Decryption (Off-Chain) - -The auditor decrypts using their spending key `sk_A`: - -```typescript -import { buildBabyjub, buildPoseidon } from "circomlibjs"; - -const BN254_P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n; - -async function decrypt( - sk_A: bigint, - epk_x: bigint, - epk_y: bigint, - enc_value: bigint, - enc_asset_id: bigint, - enc_owner_hash: bigint -) { - const babyJub = await buildBabyjub(); - const poseidon = await buildPoseidon(); - const F = poseidon.F; - - // Shared secret: sk_A · epk - const epkPoint = [babyJub.F.e(epk_x.toString()), babyJub.F.e(epk_y.toString())]; - const shared = babyJub.mulPointEscalar(epkPoint, sk_A); - const sx = BigInt(babyJub.F.toString(shared[0])); - const sy = BigInt(babyJub.F.toString(shared[1])); - - // Keystream - const k0 = BigInt(F.toString(poseidon([sx, sy, 0n]))); - const k1 = BigInt(F.toString(poseidon([sx, sy, 1n]))); - const k2 = BigInt(F.toString(poseidon([sx, sy, 2n]))); - - // Decrypt (field subtraction mod p) - const sub = (enc: bigint, k: bigint) => (enc - k + BN254_P) % BN254_P; - - return { - value: sub(enc_value, k0), // 0 if field not disclosed - asset_id: sub(enc_asset_id, k1), // 0 if field not disclosed - owner_hash: sub(enc_owner_hash, k2), // 0 if field not disclosed - }; -} -``` - -**Note**: A decrypted result of `0` means the field was not disclosed. The auditor cannot distinguish "value is 0" from "value was hidden" without a separate proof — this is intentional. - -## Usage Examples - -### Reveal Value Only - -```typescript -const input = { - // Public inputs - commitment: noteCommitment, - auditor_pk_x: auditorKey.x.toString(), - auditor_pk_y: auditorKey.y.toString(), - - // Private inputs - value: note.value.toString(), - asset_id: note.assetId.toString(), - owner_pubkey: note.ownerPubkey.toString(), - blinding: note.blinding.toString(), - disclose_value: "1", - disclose_asset_id: "0", - disclose_owner: "0", - r: ephemeralScalar.toString(), -}; -// Outputs: epk_x, epk_y, enc_value (real), enc_asset_id (zero-masked), enc_owner_hash (zero-masked) -``` - -### Reveal All Fields - -```typescript -const input = { - commitment: noteCommitment, - auditor_pk_x: auditorKey.x.toString(), - auditor_pk_y: auditorKey.y.toString(), - value: note.value.toString(), - asset_id: note.assetId.toString(), - owner_pubkey: note.ownerPubkey.toString(), - blinding: note.blinding.toString(), - disclose_value: "1", - disclose_asset_id: "1", - disclose_owner: "1", - r: ephemeralScalar.toString(), -}; -``` - -### Zero-Knowledge Proof of Ownership (Nothing Revealed) - -```typescript -const input = { - commitment: noteCommitment, - auditor_pk_x: auditorKey.x.toString(), - auditor_pk_y: auditorKey.y.toString(), - value: note.value.toString(), - asset_id: note.assetId.toString(), - owner_pubkey: note.ownerPubkey.toString(), - blinding: note.blinding.toString(), - disclose_value: "0", - disclose_asset_id: "0", - disclose_owner: "0", - r: ephemeralScalar.toString(), -}; -// All enc_* outputs contain only keystream noise — auditor learns nothing. -``` - -## Use Cases - -1. **Compliance Auditing**: Reveal value and asset to a regulator without exposing the owner. -2. **Selective Tax Reporting**: Reveal value and asset for a specific jurisdiction's auditor. -3. **Ownership Proof**: Prove note ownership to a counterparty without revealing amount. -4. **Private Escrow Dispute**: Reveal all fields to an arbitrator under ECDH confidentiality. -5. **Multi-Auditor**: Generate separate proofs with different `r` and `auditor_pk` for each auditor. - -## Security Considerations - -### Ephemeral Scalar r - -- `r` MUST be sampled uniformly at random from the BN254 scalar field for each proof. -- Reusing `r` with the same `auditor_pk` leaks the same `epk` and shared secret — breaking ciphertext unlinkability. - -### Auditor Public Key Validation - -- The circuit does NOT verify that `auditor_pk` is a valid Baby Jubjub point (this would cost ~2,500 extra constraints). The prover is responsible for using a valid curve point. -- An invalid point causes `EscalarMulAny` to produce an incorrect result, invalidating the ciphertext but not breaking soundness. - -### Owner Hash vs Raw Pubkey - -- When `disclose_owner=1`, the circuit encrypts `Poseidon(owner_pubkey)`, not the raw pubkey. This prevents the auditor from recovering the pubkey even with a weak shared secret. - -### Commitment Validation - -The runtime SHOULD verify before accepting a disclosure proof: - -1. The commitment exists in the on-chain Merkle tree. -2. The commitment has not been nullified (note not spent). - -## Implementation Notes - -Circom includes used: - -- `circomlib/circuits/poseidon.circom` — Poseidon hash (2-input and 4-input) -- `circomlib/circuits/bitify.circom` — `Num2Bits(253)` for scalar `r` -- `circomlib/circuits/escalarmulfix.circom` — `epk = r·G` -- `circomlib/circuits/escalarmulany.circom` — `shared = r·pk_A` From bbf657a1783e3dbe9195958d2f74712876c52e7d Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:03 -0400 Subject: [PATCH 16/23] Feat: update Note circuit documentation to replace Disclosure with Value Proof circuit --- docs/circuits/note.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/circuits/note.md b/docs/circuits/note.md index 296cb8b..a9c2eb2 100644 --- a/docs/circuits/note.md +++ b/docs/circuits/note.md @@ -68,7 +68,7 @@ template NoteCommitment() { Used by: -- **Disclosure**: Verify commitment matches revealed fields +- **Value Proof**: Verify commitment matches declared note fields - **Transfer**: Compute input/output note commitments - **Unshield**: Verify note commitment exists in tree @@ -475,6 +475,6 @@ Enable stateless nullifier verification: ## Related Documentation - [Poseidon Wrapper](poseidon-wrapper.md) - Poseidon hash implementations -- [Disclosure Circuit](disclosure.md) - Uses NoteCommitment +- [Value Proof Circuit](value_proof.md) - Uses NoteCommitment - [Transfer Circuit](transfer.md) - Uses both NoteCommitment and Nullifier - [Unshield Circuit](unshield.md) - Uses both NoteCommitment and Nullifier From 946c2b65d37dfd4f41fba44fc0c2792916e271b9 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:10 -0400 Subject: [PATCH 17/23] Feat: update README to replace Disclosure circuit with Value Proof circuit --- docs/circuits/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/circuits/README.md b/docs/circuits/README.md index 547299a..20013c2 100644 --- a/docs/circuits/README.md +++ b/docs/circuits/README.md @@ -6,7 +6,7 @@ This directory contains detailed technical documentation for each zero-knowledge ### Core Privacy Circuits -- **[Disclosure](disclosure.md)** - Selective disclosure of note properties with privacy preservation +- **[Value Proof](value_proof.md)** - Prove note formation (value + asset_id encoded in commitment) for relay-fee claiming - **[Transfer](transfer.md)** - Private token transfers with BabyPbk ownership verification (discrete log proof) - **[Unshield](unshield.md)** - Convert private notes to public tokens (withdrawal) - **[Private Link](private-link.md)** - Prove knowledge of a private cross-chain wallet link without revealing the address @@ -65,7 +65,7 @@ Each circuit document includes: | Circuit | Constraints | Public Inputs | Private Inputs | Tree Depth | | ------------ | ----------- | ------------- | ------------------- | ---------- | -| Disclosure | 1,584 | 4 | 7 | N/A | +| Value Proof | ~300 | 3 | 2 | N/A | | Transfer | 33,687 | 7 | 9 (+40 Merkle path) | 20 | | Unshield | 16,033 | 6 | 6 (+40 Merkle path) | 20 | | Private Link | 487 | 2 | 3 | N/A | From 2098b624900c6c771ecc0528f0337afd116a6354 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:15 -0400 Subject: [PATCH 18/23] Feat: update Poseidon wrapper documentation to replace Disclosure Circuit with Value Proof Circuit --- docs/circuits/poseidon-wrapper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/circuits/poseidon-wrapper.md b/docs/circuits/poseidon-wrapper.md index 4e24b9d..54e7551 100644 --- a/docs/circuits/poseidon-wrapper.md +++ b/docs/circuits/poseidon-wrapper.md @@ -529,7 +529,7 @@ interface PoseidonHash { - [Note Circuit](note.md) - Uses Poseidon2 and Poseidon4 - [Merkle Tree](merkle-tree.md) - Uses Poseidon2 -- [Disclosure Circuit](disclosure.md) - Uses Poseidon variants +- [Value Proof Circuit](value_proof.md) - Uses Poseidon variants - [Transfer Circuit](transfer.md) - Uses Poseidon variants - [Unshield Circuit](unshield.md) - Uses Poseidon variants From c3cee14fc34e47bddcf5e41a494dad4fe497766c Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:21 -0400 Subject: [PATCH 19/23] Feat: add Value Proof circuit documentation --- docs/circuits/value_proof.md | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/circuits/value_proof.md diff --git a/docs/circuits/value_proof.md b/docs/circuits/value_proof.md new file mode 100644 index 0000000..8fdf52d --- /dev/null +++ b/docs/circuits/value_proof.md @@ -0,0 +1,148 @@ +# Value Proof Circuit + +**File**: [`circuits/value_proof.circom`](../../circuits/value_proof.circom) + +## Purpose + +The Value Proof circuit lets a relayer **prove that a note commitment encodes exactly the declared amount** before the runtime inserts it into the Merkle tree and credits relay fees. + +Used by `pallet-shielded-pool::claim_shielded_fees`. Without this proof, a relayer could craft a commitment encoding an inflated value (e.g. 10× the pending fees) and later `unshield` that inflated amount, draining other users' funds. + +## Circuit Statement + +> "I know a note preimage `(value, asset_id, owner_pubkey, blinding)` such that `Poseidon(value, asset_id, owner_pubkey, blinding) == commitment`. The declared public `value` and `asset_id` match that preimage exactly." + +## Security Properties + +- **Soundness**: Cannot supply a commitment built with a different value than the one declared in the public signals. +- **Privacy**: `owner_pubkey` remains private; only its Poseidon hash is revealed, preventing linkage to the Baby Jubjub key. +- **Inflation prevention**: The circuit enforces `commitment == NoteCommitment(value, ...)` — a relayer cannot claim `value=10000` if the commitment was built with `value=1000`. +- **No spending key required**: Proves note formation only, not ownership or Merkle membership. + +## Public Inputs + +| Signal | Type | On-chain bytes | Description | +| ------------ | ----- | -------------- | ---------------------------------------- | +| `commitment` | Field | `[0..32]` | Note commitment (inserted into the tree) | +| `value` | Field | `[32..40]` | Declared relay fee amount (u64 LE) | +| `asset_id` | Field | `[40..44]` | Asset identifier (u32 LE) | + +## Public Outputs + +| Signal | Type | On-chain bytes | Description | +| ------------ | ----- | -------------- | ----------------------------------------- | +| `owner_hash` | Field | `[44..76]` | `Poseidon(owner_pubkey)` — auxiliary hash | + +**On-chain public signals layout (76 bytes total):** + +``` +commitment[0..32] | value[32..40] | asset_id[40..44] | owner_hash[44..76] +``` + +The runtime enforces: + +1. `public_signals[0..32] == commitment` (arg match) +2. `public_signals[32..40] == amount` (value match, u64 LE) +3. `public_signals[40..44] == asset_id` (asset match, u32 LE) + +`owner_hash` is available for off-chain audit but is not enforced by the pallet. + +## Private Inputs + +| Signal | Type | Description | +| -------------- | ----- | ----------------------------------------- | +| `owner_pubkey` | Field | Owner's Baby Jubjub public key (Ax) | +| `blinding` | Field | Random blinding factor for the commitment | + +## Constraints + +### 1. Commitment Verification + +```circom +component note_commitment = NoteCommitment(); +note_commitment.value <== value; +note_commitment.asset_id <== asset_id; +note_commitment.owner_pubkey <== owner_pubkey; +note_commitment.blinding <== blinding; +note_commitment.commitment === commitment; +``` + +Expands to: + +``` +commitment == Poseidon(value, asset_id, owner_pubkey, blinding) +``` + +### 2. Owner Hash + +```circom +component hasher = Poseidon(1); +hasher.inputs[0] <== owner_pubkey; +owner_hash <== hasher.out; +``` + +``` +owner_hash = Poseidon(owner_pubkey) +``` + +## Circuit Parameters + +- **Constraints**: ~300 (estimate — depends on Poseidon round constants) +- **Public Inputs**: 3 +- **Public Outputs**: 1 +- **Private Inputs**: 2 +- **Proving Time**: <50 ms (local machine) +- **Verification Time**: <5 ms +- **CircuitId (runtime)**: `6` (`CircuitId::VALUE_PROOF`) + +## Comparison with Other Circuits + +| Property | value_proof | unshield | +| --------------------- | ----------- | -------- | +| Merkle proof | No | Yes | +| Spending key | No | Yes | +| Nullifier | No | Yes | +| Proves note formation | Yes | Implicit | +| Proves ownership | No | Yes | +| ECDH encryption | No | No | + +## Usage Example + +```typescript +import { buildPoseidon } from "circomlibjs"; + +const poseidon = await buildPoseidon(); +const F = poseidon.F; + +const value = 1000n; +const asset_id = 0n; +const ownerPubkey = myBabyJubKeyAx; +const blinding = crypto.randomBytes(31).readBigUInt64BE(); + +const commitment = BigInt(F.toString(poseidon([value, asset_id, ownerPubkey, blinding]))); + +const input = { + // Public inputs + commitment: commitment.toString(), + value: value.toString(), + asset_id: asset_id.toString(), + // Private inputs + owner_pubkey: ownerPubkey.toString(), + blinding: blinding.toString(), +}; + +// Generate proof (proof-generator or snarkjs) +// const { proof, publicSignals } = await groth16.fullProve(input, wasmPath, zkeyPath); +``` + +## Inflation Attack Prevention + +Without this proof, the following attack is possible: + +1. Relayer has 1,000 pending fees for asset 0. +2. Relayer builds `commitment = Poseidon(10_000, 0, pk, r)` (inflated value). +3. Relayer calls `claim_shielded_fees(amount=1_000, commitment=...)`. +4. Runtime credits only 1,000 from the pool balance but inserts a note worth 10,000. +5. Relayer calls `unshield(commitment)` and withdraws 10,000 — stealing 9,000 from other users. + +With `value_proof`, step 3 requires a valid Groth16 proof where `commitment` was built with `value=1_000`. The circuit constraint `commitment === NoteCommitment(1_000, ...)` makes it impossible to supply a commitment built for 10,000 while declaring 1,000. From b83715859a237f42f1797ec9976b7911569883d6 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:28 -0400 Subject: [PATCH 20/23] Feat: update Arkworks integration guide to replace Disclosure circuit with Value Proof circuit --- docs/guides/arkworks-integration.md | 64 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/guides/arkworks-integration.md b/docs/guides/arkworks-integration.md index 6437500..3ee418a 100644 --- a/docs/guides/arkworks-integration.md +++ b/docs/guides/arkworks-integration.md @@ -51,14 +51,14 @@ The pipeline will automatically: Convert a specific circuit: ```bash -# Disclosure circuit -pnpm run convert:disclosure +# Value Proof circuit +pnpm run convert:value-proof # Transfer circuit pnpm run convert:transfer # Or use the script directly -bash scripts/build/convert-to-ark.sh disclosure +bash scripts/build/convert-to-ark.sh value_proof ``` ### Generated Files @@ -67,8 +67,8 @@ After successful conversion: ``` keys/ -├── disclosure_pk.zkey # 689KB - For JavaScript/TypeScript -└── disclosure_pk.ark # 689KB - For Rust +├── value_proof_pk.zkey # For JavaScript/TypeScript +└── value_proof_pk.ark # For Rust ``` ## CI/CD Pipeline @@ -90,19 +90,19 @@ Creates two release packages: **JavaScript/TypeScript Package:** ```bash -disclosure-circuit-js-v0.1.0.tar.gz -├── disclosure.wasm -├── disclosure_pk.zkey -└── verification_key_disclosure.json +value-proof-circuit-js-v0.1.0.tar.gz +├── value_proof.wasm +├── value_proof_pk.zkey +└── verification_key_value_proof.json ``` **Rust Package:** ```bash -disclosure-circuit-rust-v0.1.0.tar.gz -├── disclosure.wasm -├── disclosure_pk.ark -└── verification_key_disclosure.json +value-proof-circuit-rust-v0.1.0.tar.gz +├── value_proof.wasm +├── value_proof_pk.ark +└── verification_key_value_proof.json ``` ## Usage Examples @@ -114,8 +114,8 @@ import { groth16 } from "snarkjs"; const { proof, publicSignals } = await groth16.fullProve( input, - "build/disclosure_js/disclosure.wasm", - "keys/disclosure_pk.zkey" // ← Use .zkey + "build/value_proof_js/value_proof.wasm", + "keys/value_proof_pk.zkey" // ← Use .zkey ); ``` @@ -130,7 +130,7 @@ builder.setup(); let circom = builder.build().unwrap(); let proof = circom.prove( - "keys/disclosure_pk.ark" // ← Use .ark + "keys/value_proof_pk.ark" // ← Use .ark ).unwrap(); ``` @@ -162,17 +162,17 @@ which ark-circom ```bash # 1. Verify .zkey file is valid snarkjs zkey verify \ - build/disclosure.r1cs \ + build/value_proof.r1cs \ ptau/pot16_final.ptau \ - keys/disclosure_pk.zkey + keys/value_proof_pk.zkey # 2. Check ark-circom version ark-circom --version # 3. Try manual conversion with verbose output ark-circom \ - --input keys/disclosure_pk.zkey \ - --output keys/disclosure_pk.ark + --input keys/value_proof_pk.zkey \ + --output keys/value_proof_pk.ark ``` ### Rust not installed in CI @@ -218,30 +218,30 @@ This is **normal** if ark-circom is not installed. The build pipeline gracefully **Files needed**: -- `disclosure.wasm` (witness calculator) -- `disclosure_pk.zkey` (proving key) +- `value_proof.wasm` (witness calculator) +- `value_proof_pk.zkey` (proving key) **Download**: ```bash # From GitHub release -wget https://github.com/orbinum/circuits/releases/download/v0.1.0/disclosure-circuit-js-v0.1.0.tar.gz -tar -xzf disclosure-circuit-js-v0.1.0.tar.gz +wget https://github.com/orbinum/circuits/releases/download/v0.1.0/value-proof-circuit-js-v0.1.0.tar.gz +tar -xzf value-proof-circuit-js-v0.1.0.tar.gz ``` ### Substrate Runtime (Rust) **Files needed**: -- `disclosure.wasm` (witness calculator) -- `disclosure_pk.ark` (proving key) +- `value_proof.wasm` (witness calculator) +- `value_proof_pk.ark` (proving key) **Download**: ```bash # From GitHub release -wget https://github.com/orbinum/circuits/releases/download/v0.1.0/disclosure-circuit-rust-v0.1.0.tar.gz -tar -xzf disclosure-circuit-rust-v0.1.0.tar.gz +wget https://github.com/orbinum/circuits/releases/download/v0.1.0/value-proof-circuit-rust-v0.1.0.tar.gz +tar -xzf value-proof-circuit-rust-v0.1.0.tar.gz ``` **Integration**: @@ -252,12 +252,12 @@ use ark_circom::CircomBuilder; pub fn generate_proof(input: CircuitInput) -> Result { let builder = CircomBuilder::::new( - std::include_bytes!("../circuits/disclosure.wasm") + std::include_bytes!("../circuits/value_proof.wasm") ); // Use embedded .ark key let circom = builder.setup_with_ark( - std::include_bytes!("../circuits/disclosure_pk.ark") + std::include_bytes!("../circuits/value_proof_pk.ark") )?; circom.prove(input) @@ -291,9 +291,9 @@ Currently, there's no direct verification tool for `.ark` files. Verify the sour ```bash snarkjs zkey verify \ - build/disclosure.r1cs \ + build/value_proof.r1cs \ ptau/pot16_final.ptau \ - keys/disclosure_pk.zkey + keys/value_proof_pk.zkey ``` Then convert to `.ark` - the conversion preserves cryptographic validity. From 557d3408c4a81c0078a006b183ecaf4450dbf8f9 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:33 -0400 Subject: [PATCH 21/23] Feat: update Quick Start Guide to replace Disclosure circuit with Value Proof circuit --- docs/guides/quick-start.md | 61 ++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md index ea3168d..f8d9371 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -85,52 +85,49 @@ For more control, build circuits individually: #### Step 1: Compile Circuit ```bash -# Compile disclosure circuit -pnpm run compile:disclosure +# Compile value_proof circuit +pnpm run compile:value-proof # Output: -# - build/disclosure.r1cs (208KB) -# - build/disclosure.sym (129KB) -# - build/disclosure_js/disclosure.wasm (2.1MB) +# - build/value_proof.r1cs +# - build/value_proof.sym +# - build/value_proof_js/value_proof.wasm ``` #### Step 2: Generate Keys ```bash # Generate proving and verifying keys -pnpm run setup:disclosure +pnpm run setup:value-proof # Output: -# - keys/disclosure_pk.zkey (689KB) -# - build/verification_key_disclosure.json (3.4KB) +# - keys/value_proof_pk.zkey +# - build/verification_key_value_proof.json ``` #### Step 3: Test Circuit ```bash # Run tests -pnpm test -- --grep "disclosure" +pnpm test -- --grep "ValueProof" ``` ## Your First Proof ### 1. Generate Test Input -```bash -pnpm run gen-input:disclosure -``` - -This creates sample inputs in `build/input_*.json`: - -- `reveal_nothing.json` - Full privacy mode -- `reveal_value_only.json` - Disclose amount only -- `reveal_value_and_asset.json` - Disclose amount + asset -- `reveal_all.json` - Complete disclosure +Build a valid input manually using `circomlibjs` (see [value_proof.md](../circuits/value_proof.md#usage-example) for the full snippet) or copy from `test/value_proof.test.ts`. ### 2. Generate Proof ```bash -pnpm run prove:disclosure +# Using snarkjs directly +npx snarkjs groth16 fullprove \ + build/value_proof_input.json \ + build/value_proof_js/value_proof.wasm \ + keys/value_proof_pk.zkey \ + build/proof.json \ + build/public.json ``` This generates: @@ -138,16 +135,14 @@ This generates: - `build/proof.json` - The zero-knowledge proof - `build/public.json` - Public signals -**Expected time**: ~100-150ms +**Expected time**: <50ms ### 3. Verify Proof -The proof is automatically verified during generation. You can also verify manually: - ```bash # Using snarkjs directly npx snarkjs groth16 verify \ - build/verification_key_disclosure.json \ + build/verification_key_value_proof.json \ build/public.json \ build/proof.json ``` @@ -168,7 +163,7 @@ pnpm test ```bash # Test a specific circuit -pnpm test -- --grep "disclosure" +pnpm test -- --grep "ValueProof" # Test a specific component pnpm test -- --grep "merkle" @@ -178,7 +173,7 @@ pnpm test -- --grep "merkle" | Test Suite | Tests | Purpose | | --------------------- | ----- | ---------------------------------------------------- | -| `disclosure.test.ts` | 12 | Selective disclosure logic | +| `value_proof.test.ts` | 16 | Note formation proof, inflation attack prevention | | `transfer.test.ts` | 79 | Private transfer validation | | `unshield.test.ts` | 44 | Asset unshielding (total + partial with change note) | | `merkle_tree.test.ts` | 15 | Merkle proof verification | @@ -190,9 +185,6 @@ pnpm test -- --grep "merkle" ### Run Benchmarks ```bash -# Benchmark disclosure circuit -pnpm run bench:disclosure - # Benchmark all circuits pnpm run bench ``` @@ -200,14 +192,13 @@ pnpm run bench ### Typical Results ``` -📊 Disclosure Circuit Benchmarks - Witness Generation: 5.23ms avg - Proof Generation: 101.29ms avg - Verification: 3.87ms avg - Throughput: 9.87 proofs/sec +📊 Value Proof Circuit Benchmarks + Witness Generation: <5ms avg + Proof Generation: <50ms avg + Verification: <5ms avg ``` -Results saved to: `build/benchmark_results_disclosure.json` +Results saved to: `build/benchmark_results_value_proof.json` ## Common Tasks From c41b9db46ab0e52d873f2a8e68ce32ae774d1f1c Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:49 -0400 Subject: [PATCH 22/23] Feat: update circuit references to replace Disclosure circuit with Value Proof circuit --- npm/README.md | 12 ++++++------ npm/index.d.ts | 4 ++-- npm/index.js | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/npm/README.md b/npm/README.md index 0832f3b..88a4ad0 100644 --- a/npm/README.md +++ b/npm/README.md @@ -13,9 +13,9 @@ npm install @orbinum/circuits ## 📦 Package Contents -This package includes **20 files** for 4 circuits (disclosure, transfer, unshield, private_link): +This package includes **20 files** for 4 circuits (value_proof, transfer, unshield, private_link): -### For Each Circuit (disclosure, transfer, unshield, private_link): +### For Each Circuit (value_proof, transfer, unshield, private_link): 1. **`{circuit}.wasm`** - Witness calculator (4 files) 2. **`{circuit}.r1cs`** - R1CS constraint system — for custom provers / verification (4 files) @@ -33,7 +33,7 @@ import { readFileSync } from "fs"; import { getCircuitPaths } from "@orbinum/circuits"; // Get all paths for a circuit -const paths = getCircuitPaths("transfer"); // 'disclosure' | 'transfer' | 'unshield' | 'private_link' +const paths = getCircuitPaths("transfer"); // 'value_proof' | 'transfer' | 'unshield' | 'private_link' // Load WASM witness calculator const wasmBuffer = readFileSync(paths.wasm); @@ -73,9 +73,9 @@ import verificationKey from "@orbinum/circuits/verification_key_transfer.json"; ## 📋 Available Circuits -### 1. **Disclosure** (`disclosure_*`) +### 1. **Value Proof** (`value_proof_*`) -Selective disclosure circuit for privacy-preserving attribute revelation. +Proves that a note commitment encodes exactly the declared relay fee amount. Used by `pallet-shielded-pool::claim_shielded_fees` to prevent inflation attacks. `CircuitId = 6`. ### 2. **Transfer** (`transfer_*`) @@ -100,7 +100,7 @@ Proves linkage between two commitments without revealing their values. import { generateProof, CircuitType } from "@orbinum/proof-generator"; // Proof generator automatically loads circuits from @orbinum/circuits -const result = await generateProof(CircuitType.Transfer, witnessInputs, numPublicSignals); +const result = await generateProof(CircuitType.ValueProof, witnessInputs, numPublicSignals); console.log("Proof:", result.proof); console.log("Public signals:", result.publicSignals); diff --git a/npm/index.d.ts b/npm/index.d.ts index 466e308..d2f07aa 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -16,12 +16,12 @@ export interface CircuitPaths { * Get paths to all files for a specific circuit */ export function getCircuitPaths( - circuit: "disclosure" | "transfer" | "unshield" | "private_link" + circuit: "value_proof" | "transfer" | "unshield" | "private_link" ): CircuitPaths; /** * Available circuits */ -export type CircuitType = "disclosure" | "transfer" | "unshield" | "private_link"; +export type CircuitType = "value_proof" | "transfer" | "unshield" | "private_link"; export const CIRCUITS: CircuitType[]; diff --git a/npm/index.js b/npm/index.js index db24c63..77ea8f6 100644 --- a/npm/index.js +++ b/npm/index.js @@ -6,11 +6,11 @@ const { join } = require("path"); -const CIRCUITS = ["disclosure", "transfer", "unshield", "private_link"]; +const CIRCUITS = ["value_proof", "transfer", "unshield", "private_link"]; /** * Get paths to all files for a specific circuit - * @param {string} circuit - Circuit name: 'disclosure', 'transfer', 'unshield', or 'private_link' + * @param {string} circuit - Circuit name: 'value_proof', 'transfer', 'unshield', or 'private_link' * @returns {Object} Paths to circuit files */ function getCircuitPaths(circuit) { From d07d7e717a5d4f4c7e6aceaba942cd8f3758783e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Thu, 14 May 2026 20:40:54 -0400 Subject: [PATCH 23/23] Feat: update circuit references to replace Disclosure circuit with Value Proof circuit in manifest generation --- scripts/utils/generate-manifest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/utils/generate-manifest.ts b/scripts/utils/generate-manifest.ts index 20c1777..d03dd4a 100644 --- a/scripts/utils/generate-manifest.ts +++ b/scripts/utils/generate-manifest.ts @@ -4,7 +4,7 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; -type CircuitName = "disclosure" | "transfer" | "unshield" | "private_link"; +type CircuitName = "value_proof" | "transfer" | "unshield" | "private_link"; type ArtifactKind = "wasm" | "zkey" | "ark" | "r1cs" | "vk_json"; interface ArtifactEntry { @@ -48,7 +48,7 @@ if (!Number.isFinite(defaultCircuitVersion) || defaultCircuitVersion < 1) { throw new Error(`Invalid CIRCUIT_VERSION: ${process.env.CIRCUIT_VERSION}`); } -const circuits: CircuitName[] = ["disclosure", "transfer", "unshield", "private_link"]; +const circuits: CircuitName[] = ["value_proof", "transfer", "unshield", "private_link"]; function sha256Hex(data: Buffer): string { return crypto.createHash("sha256").update(data).digest("hex");