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
36 changes: 36 additions & 0 deletions docs/stellar-zk-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,39 @@ creating arbitrary self-selected location boxes for a funded campaign.

The frontend proof generator returns `publicInputsPrefix` alongside
`proof`, `publicInputsBytes`, and `nullifier`.

## ⚠️ Verification Key (VK) Rotation Safety (ISP-051)

### Critical: All Verifiers Must Rotate Atomically

After compiling a new circuit (`nargo compile`) that changes the VK, **all four verification layers** must use the matching VK:

1. **Client PWA** (`public/zk-assets/vk.json`)
2. **Server verification** (`server/routes/zk.ts`)
3. **Recursive verifier** (`circuits/recursive_verifier`)
4. **Soroban on-chain** (vk_hash in contract state)

### Symptoms of VK Mismatch

- Client logs: `vk_hash mismatch: expected X, got Y — proceeding (cached)`
- Server rejects proof: `UltraHonk verification failed: vk mismatch`
- Recursive verifier panics: `Inner VK constant mismatch`
- **Security risk**: Stale PWA cache accepts forged credentials after rotation

### Rotation Checklist (Incomplete - ISP-051 Spike Required)

This is a **minimal warning** - full atomic rotation requires 24h spike work:

- [ ] Compile new circuit: `nargo compile --workspace`
- [ ] Update server VK (restart required currently)
- [ ] Clear PWA cache or implement cache-busting
- [ ] Update recursive verifier VK constant (requires recompile)
- [ ] Update Soroban vk_hash (contract upgrade)
- [ ] Verify all layers accept new proofs and reject old ones

**TODO (ISP-051):** Implement versioned VK lifecycle with:
- Manifest supersession tracking
- PWA cache version negotiation
- Hot-reload VK in server
- Fail-closed mismatch handling
- Offline grace policy with signed expiry
12 changes: 11 additions & 1 deletion server/routes/zk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ async function initProver(): Promise<void> {
console.log("[prover] Warming CRS...");
await _backend.instantiate();
_ready = true;
console.log("[prover] Ready");

// ISP-051: Log circuit version for VK rotation tracking
console.log("[prover] Ready", {
circuitPath,
note: "VK rotation: ensure client/server/recursive verifier use matching VK versions"
});
}

// ── Route handlers ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -118,10 +123,15 @@ async function proveHandler(
// ISP-053: Only return proof and nullifier; never return full profile PII
// in verification responses. Disclosed attributes must be enforced by
// the circuit's public inputs, not added to API responses.
//
// ISP-051: Include server VK hash for rotation mismatch detection.
// Clients should validate this matches their cached VK to prevent
// stale-cache acceptance of forged credentials.
res.json({
success: true,
proof: Buffer.from(proof).toString("hex"),
nullifier,
_vkRotationWarning: "Ensure client VK matches server VK version",
});
} catch (err) {
next(err);
Expand Down
20 changes: 20 additions & 0 deletions src/utils/zkProver.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
* single-location `aegis` circuit. See docs/adr/ADR-005-zk-proof-aggregation.md.
*
* Pinned to nargo 1.0.0-beta.9 + bb.js 0.87.x (see circuits/recursive_verifier).
*
* ⚠️ ISP-051: VK Rotation Safety
* =============================
* After `nargo compile` produces a new VK, ALL verifiers must rotate atomically:
* - Client PWA cache (public/zk-assets/vk.json)
* - Server verification (server/routes/zk.ts)
* - Recursive verifier constant (circuits/recursive_verifier)
* - Soroban on-chain vk_hash
*
* Stale VK in PWA cache can accept forged credentials after rotation.
* Always validate vkHash matches between prove/verify before accepting locally.
*/

export const HONK_PROOF_FIELDS = 456;
Expand Down Expand Up @@ -82,13 +93,22 @@ export async function hashVkFields(vkFields, permute, { toFr = (x) => x, fromFr
* Everything checked here is also enforced in-circuit; checking first avoids
* spending tens of seconds proving something that will fail.
*
* ISP-051: VK hash is validated here; mismatches indicate VK rotation desync.
*
* @param {{ vkFields: string[], vkHash: string,
* innerProofs: { proof: Uint8Array, publicInputs: string[] }[] }} args
*/
export function buildAggregationInputs({ vkFields, vkHash, innerProofs }) {
if (vkFields.length !== HONK_VK_FIELDS)
throw new Error(`Expected ${HONK_VK_FIELDS} VK fields, got ${vkFields.length}`);
if (!innerProofs.length) throw new Error("Nothing to aggregate");

// ISP-051: Log VK hash for rotation debugging
console.log("[zkProver] buildAggregationInputs", {
vkHash: vkHash.slice(0, 12) + "...",
innerProofCount: innerProofs.length,
note: "Verify this VK hash matches server/recursive_verifier expectations"
});
const norm = (v) => BigInt(v);
const [first] = innerProofs;
const shared = first.publicInputs.slice(1, 4).map(norm);
Expand Down