diff --git a/src/components/KaiVoh/KaiVohApp.tsx b/src/components/KaiVoh/KaiVohApp.tsx index 986f11c11..efbfaefad 100644 --- a/src/components/KaiVoh/KaiVohApp.tsx +++ b/src/components/KaiVoh/KaiVohApp.tsx @@ -47,6 +47,7 @@ import { computeBundleHash, hashProofCapsuleV1, hashSvgText, + normalizeProofBundleZkCurves, normalizeChakraDay, PROOF_CANON, PROOF_BINDINGS, @@ -826,11 +827,13 @@ function KaiVohFlow(): ReactElement { const zkMeta = zkPoseidonHash ? { protocol: "groth16", - curve: "BLS12-381", scheme: "groth16-poseidon", circuitId: "sigil_proof", } : undefined; + const normalizedZk = normalizeProofBundleZkCurves({ zkProof, zkMeta }); + zkProof = normalizedZk.zkProof; + const zkMetaNormalized = normalizedZk.zkMeta; const proofBundleBase = { v: "KPB-1", hashAlg: PROOF_HASH_ALG, @@ -843,7 +846,7 @@ function KaiVohFlow(): ReactElement { zkPoseidonHash, zkProof, zkPublicInputs, - zkMeta, + zkMeta: zkMetaNormalized, }; const transport = { shareUrl, diff --git a/src/components/KaiVoh/verifierProof.ts b/src/components/KaiVoh/verifierProof.ts index c842201a7..5136c93cd 100644 --- a/src/components/KaiVoh/verifierProof.ts +++ b/src/components/KaiVoh/verifierProof.ts @@ -43,12 +43,14 @@ export type ZkStatement = { domainTag: string; publicInputsContract?: ZkPublicInputsContract; }; +export type ZkCurve = "bn128" | "BLS12-381" | (string & {}); export type ZkMeta = Readonly<{ protocol?: string; - curve?: string; + curve?: ZkCurve; scheme?: string; circuitId?: string; vkHash?: string; + warnings?: string[]; }>; export type VerificationCache = Readonly<{ zkVerifiedCached?: boolean; @@ -157,6 +159,23 @@ export function normalizeChakraDay(v?: string): ChakraDay | undefined { return CHAKRA_MAP[k]; } +/* -------------------------------------------------------------------------- */ +/* ZK curve normalization */ +/* -------------------------------------------------------------------------- */ + +export function normalizeZkCurve(curve: string): ZkCurve { + const trimmed = curve.trim(); + if (!trimmed) return trimmed; + const normalized = trimmed.toLowerCase(); + if (normalized === "bn128" || normalized === "altbn128" || normalized === "bn254") { + return "bn128"; + } + if (normalized === "bls12-381") { + return "BLS12-381"; + } + return trimmed; +} + /* -------------------------------------------------------------------------- */ /* Proof Capsule Hash (KPV-1) */ /* -------------------------------------------------------------------------- */ @@ -274,6 +293,39 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +type ZkProofWithCurve = Readonly & { curve?: ZkCurve }>; + +export function normalizeProofBundleZkCurves(input: { + zkProof?: unknown; + zkMeta?: ZkMeta; +}): { zkProof?: unknown; zkMeta?: ZkMeta; curve?: ZkCurve } { + const { zkProof, zkMeta } = input; + if (!isRecord(zkProof) || typeof zkProof.curve !== "string") { + return { zkProof, zkMeta }; + } + + const normalizedCurve = normalizeZkCurve(zkProof.curve); + const normalizedProof: ZkProofWithCurve = { ...zkProof, curve: normalizedCurve }; + + if (!zkMeta) { + return { zkProof: normalizedProof, zkMeta, curve: normalizedCurve }; + } + + const metaCurve = typeof zkMeta.curve === "string" ? zkMeta.curve.trim() : undefined; + const warnings = Array.isArray(zkMeta.warnings) ? [...zkMeta.warnings] : []; + if (metaCurve && normalizeZkCurve(metaCurve) !== normalizedCurve) { + warnings.push(`curve_mismatch_corrected: meta=${metaCurve} proof=${normalizedCurve}`); + } + + const normalizedMeta: ZkMeta = { + ...zkMeta, + curve: normalizedCurve, + warnings: warnings.length ? warnings : undefined, + }; + + return { zkProof: normalizedProof, zkMeta: normalizedMeta, curve: normalizedCurve }; +} + function dropUndefined>(value: T): T { const entries = Object.entries(value).filter((entry) => entry[1] !== undefined); return Object.fromEntries(entries) as T; @@ -340,6 +392,9 @@ export function buildBundleRoot(bundle: ProofBundleLike): BundleRoot { } : undefined); const zkStatement = zkStatementBase ? withZkPublicInputsContract(zkStatementBase) : undefined; + const normalizedZk = normalizeProofBundleZkCurves({ zkProof: bundle.zkProof, zkMeta: bundle.zkMeta }); + const zkProof = normalizedZk.zkProof ?? bundle.zkProof; + const zkMeta = normalizedZk.zkMeta ?? bundle.zkMeta; return dropUndefined({ v: typeof bundle.v === "string" ? bundle.v : undefined, hashAlg, @@ -350,9 +405,9 @@ export function buildBundleRoot(bundle: ProofBundleLike): BundleRoot { capsuleHash: bundle.capsuleHash, svgHash: bundle.svgHash, zkPoseidonHash: bundle.zkPoseidonHash, - zkProof: bundle.zkProof, + zkProof, zkPublicInputs: bundle.zkPublicInputs, - zkMeta: bundle.zkMeta, + zkMeta, }); } diff --git a/src/components/SigilModal.tsx b/src/components/SigilModal.tsx index f55d87077..3b825cf36 100644 --- a/src/components/SigilModal.tsx +++ b/src/components/SigilModal.tsx @@ -47,6 +47,7 @@ import { computeBundleHash, hashProofCapsuleV1, hashSvgText, + normalizeProofBundleZkCurves, normalizeChakraDay, PROOF_CANON, PROOF_BINDINGS, @@ -1447,11 +1448,13 @@ const SigilModal: FC = ({ onClose }: Props) => { const zkMeta = zkPoseidonHash ? { protocol: "groth16", - curve: "BLS12-381", scheme: "groth16-poseidon", circuitId: "sigil_proof", } : undefined; + const normalizedZk = normalizeProofBundleZkCurves({ zkProof, zkMeta }); + zkProof = normalizedZk.zkProof; + const zkMetaNormalized = normalizedZk.zkMeta; const proofBundleBase = { hashAlg: PROOF_HASH_ALG, canon: PROOF_CANON, @@ -1463,7 +1466,7 @@ const SigilModal: FC = ({ onClose }: Props) => { zkPoseidonHash, zkProof, zkPublicInputs, - zkMeta, + zkMeta: zkMetaNormalized, }; const transport = { shareUrl, diff --git a/src/pages/SigilPage/exportZip.ts b/src/pages/SigilPage/exportZip.ts index bf06c174a..ffc4b3565 100644 --- a/src/pages/SigilPage/exportZip.ts +++ b/src/pages/SigilPage/exportZip.ts @@ -25,6 +25,7 @@ import { computeBundleHash, hashProofCapsuleV1, hashSvgText, + normalizeProofBundleZkCurves, normalizeChakraDay, PROOF_CANON, PROOF_BINDINGS, @@ -599,11 +600,13 @@ export async function exportZIP(ctx: { const zkMeta = zkPoseidonHash ? { protocol: "groth16", - curve: "BLS12-381", scheme: "groth16-poseidon", circuitId: "sigil_proof", } : undefined; + const normalizedZk = normalizeProofBundleZkCurves({ zkProof, zkMeta }); + zkProof = normalizedZk.zkProof; + const zkMetaNormalized = normalizedZk.zkMeta; const proofBundleBase = { hashAlg: PROOF_HASH_ALG, canon: PROOF_CANON, @@ -615,7 +618,7 @@ export async function exportZIP(ctx: { zkPoseidonHash, zkProof, zkPublicInputs, - zkMeta, + zkMeta: zkMetaNormalized, }; const transport = { diff --git a/tests/sigil_bundle_pipeline.test.mjs b/tests/sigil_bundle_pipeline.test.mjs index e3a4e8f39..8b0df9119 100644 --- a/tests/sigil_bundle_pipeline.test.mjs +++ b/tests/sigil_bundle_pipeline.test.mjs @@ -108,12 +108,15 @@ const kai = await import(pathToFileURL(transpileRecursive(kaiPath.href)).href); const { embedProofMetadata } = svgProof; const { + buildBundleRoot, buildBundleUnsigned, hashBundle, hashProofCapsuleV1, hashSvgText, + normalizeProofBundleZkCurves, PROOF_CANON, PROOF_HASH_ALG, + ZK_PUBLIC_INPUTS_CONTRACT, } = verifier; const { extractProofBundleMetaFromSvg } = meta; const { verifyBundleAuthorSig } = kas; @@ -232,3 +235,36 @@ test("sigil proof bundle hashes/signature stay deterministic with zk proof", asy const zkVerified = await groth16.verify(vkey, zkPublicInputs, zkProof); assert.equal(zkVerified, true); }); + +test("proof bundle normalizes zk curve metadata", () => { + const zkProof = { + curve: "bn128", + pi_a: ["1", "2", "3"], + }; + const zkMeta = { + protocol: "groth16", + curve: "BLS12-381", + scheme: "groth16-poseidon", + circuitId: "sigil_proof", + }; + + const normalized = normalizeProofBundleZkCurves({ zkProof, zkMeta }); + const proofBundleBase = { + hashAlg: PROOF_HASH_ALG, + canon: PROOF_CANON, + zkProof: normalized.zkProof, + zkMeta: normalized.zkMeta, + }; + const bundleRoot = buildBundleRoot(proofBundleBase); + const proofBundle = { ...proofBundleBase, bundleRoot }; + + assert.equal(proofBundle.zkMeta?.curve, "bn128"); + assert.equal(proofBundle.zkProof?.curve, "bn128"); + assert.equal(proofBundle.bundleRoot?.zkMeta?.curve, "bn128"); + assert.equal(proofBundle.bundleRoot?.zkProof?.curve, "bn128"); + assert.deepEqual(proofBundle.zkMeta?.warnings, [ + "curve_mismatch_corrected: meta=BLS12-381 proof=bn128", + ]); + + assert.equal(ZK_PUBLIC_INPUTS_CONTRACT.invariant, "publicInputs[0] == publicInputs[1]"); +});