Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/KaiVoh/KaiVohApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
computeBundleHash,
hashProofCapsuleV1,
hashSvgText,
normalizeProofBundleZkCurves,
normalizeChakraDay,
PROOF_CANON,
PROOF_BINDINGS,
Expand Down Expand Up @@ -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,
Expand All @@ -843,7 +846,7 @@ function KaiVohFlow(): ReactElement {
zkPoseidonHash,
zkProof,
zkPublicInputs,
zkMeta,
zkMeta: zkMetaNormalized,
};
const transport = {
shareUrl,
Expand Down
61 changes: 58 additions & 3 deletions src/components/KaiVoh/verifierProof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -274,6 +293,39 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

type ZkProofWithCurve = Readonly<Record<string, unknown> & { 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,
Comment on lines +320 to +323
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty proof curve as missing before overwriting meta

normalizeProofBundleZkCurves always overwrites zkMeta.curve with normalizedCurve, but normalizeZkCurve returns an empty string for whitespace-only values. If a bundle arrives with a blank zkProof.curve (e.g., user-provided payload) and a valid zkMeta.curve, this logic erases the usable curve and propagates "" into the bundle root. That can break any verifier that relies on zkMeta.curve when the proof object is incomplete. Consider treating empty/whitespace curves as absent (e.g., return undefined and skip the overwrite) before setting zkMeta.curve.

Useful? React with 👍 / 👎.

};

return { zkProof: normalizedProof, zkMeta: normalizedMeta, curve: normalizedCurve };
}

function dropUndefined<T extends Record<string, unknown>>(value: T): T {
const entries = Object.entries(value).filter((entry) => entry[1] !== undefined);
return Object.fromEntries(entries) as T;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
});
}

Expand Down
7 changes: 5 additions & 2 deletions src/components/SigilModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
computeBundleHash,
hashProofCapsuleV1,
hashSvgText,
normalizeProofBundleZkCurves,
normalizeChakraDay,
PROOF_CANON,
PROOF_BINDINGS,
Expand Down Expand Up @@ -1447,11 +1448,13 @@ const SigilModal: FC<Props> = ({ 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,
Expand All @@ -1463,7 +1466,7 @@ const SigilModal: FC<Props> = ({ onClose }: Props) => {
zkPoseidonHash,
zkProof,
zkPublicInputs,
zkMeta,
zkMeta: zkMetaNormalized,
};
const transport = {
shareUrl,
Expand Down
7 changes: 5 additions & 2 deletions src/pages/SigilPage/exportZip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
computeBundleHash,
hashProofCapsuleV1,
hashSvgText,
normalizeProofBundleZkCurves,
normalizeChakraDay,
PROOF_CANON,
PROOF_BINDINGS,
Expand Down Expand Up @@ -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,
Expand All @@ -615,7 +618,7 @@ export async function exportZIP(ctx: {
zkPoseidonHash,
zkProof,
zkPublicInputs,
zkMeta,
zkMeta: zkMetaNormalized,
};

const transport = {
Expand Down
36 changes: 36 additions & 0 deletions tests/sigil_bundle_pipeline.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]");
});