@fluxpointstudios/orynq-sdk-midnight-prover 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +78 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +89 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/linking/cardano-anchor-link.d.ts +250 -0
  7. package/dist/linking/cardano-anchor-link.d.ts.map +1 -0
  8. package/dist/linking/cardano-anchor-link.js +447 -0
  9. package/dist/linking/cardano-anchor-link.js.map +1 -0
  10. package/dist/linking/index.d.ts +33 -0
  11. package/dist/linking/index.d.ts.map +1 -0
  12. package/dist/linking/index.js +31 -0
  13. package/dist/linking/index.js.map +1 -0
  14. package/dist/linking/proof-publication.d.ts +217 -0
  15. package/dist/linking/proof-publication.d.ts.map +1 -0
  16. package/dist/linking/proof-publication.js +385 -0
  17. package/dist/linking/proof-publication.js.map +1 -0
  18. package/dist/midnight/index.d.ts +30 -0
  19. package/dist/midnight/index.d.ts.map +1 -0
  20. package/dist/midnight/index.js +27 -0
  21. package/dist/midnight/index.js.map +1 -0
  22. package/dist/midnight/proof-server-client.d.ts +236 -0
  23. package/dist/midnight/proof-server-client.d.ts.map +1 -0
  24. package/dist/midnight/proof-server-client.js +422 -0
  25. package/dist/midnight/proof-server-client.js.map +1 -0
  26. package/dist/midnight/public-inputs.d.ts +134 -0
  27. package/dist/midnight/public-inputs.d.ts.map +1 -0
  28. package/dist/midnight/public-inputs.js +338 -0
  29. package/dist/midnight/public-inputs.js.map +1 -0
  30. package/dist/midnight/witness-builder.d.ts +119 -0
  31. package/dist/midnight/witness-builder.d.ts.map +1 -0
  32. package/dist/midnight/witness-builder.js +238 -0
  33. package/dist/midnight/witness-builder.js.map +1 -0
  34. package/dist/proofs/hash-chain-proof.d.ts +171 -0
  35. package/dist/proofs/hash-chain-proof.d.ts.map +1 -0
  36. package/dist/proofs/hash-chain-proof.js +437 -0
  37. package/dist/proofs/hash-chain-proof.js.map +1 -0
  38. package/dist/proofs/index.d.ts +35 -0
  39. package/dist/proofs/index.d.ts.map +1 -0
  40. package/dist/proofs/index.js +34 -0
  41. package/dist/proofs/index.js.map +1 -0
  42. package/dist/proofs/policy-compliance-proof.d.ts +165 -0
  43. package/dist/proofs/policy-compliance-proof.d.ts.map +1 -0
  44. package/dist/proofs/policy-compliance-proof.js +514 -0
  45. package/dist/proofs/policy-compliance-proof.js.map +1 -0
  46. package/dist/proofs/selective-disclosure.d.ts +213 -0
  47. package/dist/proofs/selective-disclosure.d.ts.map +1 -0
  48. package/dist/proofs/selective-disclosure.js +629 -0
  49. package/dist/proofs/selective-disclosure.js.map +1 -0
  50. package/dist/prover-interface.d.ts +288 -0
  51. package/dist/prover-interface.d.ts.map +1 -0
  52. package/dist/prover-interface.js +114 -0
  53. package/dist/prover-interface.js.map +1 -0
  54. package/dist/prover.d.ts +163 -0
  55. package/dist/prover.d.ts.map +1 -0
  56. package/dist/prover.js +417 -0
  57. package/dist/prover.js.map +1 -0
  58. package/dist/types.d.ts +410 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +128 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +59 -0
  63. package/src/__tests__/hash-chain-proof.test.ts +709 -0
  64. package/src/__tests__/midnight-prover.test.ts +716 -0
  65. package/src/__tests__/policy-compliance.test.ts +567 -0
  66. package/src/__tests__/proof-publication.test.ts +644 -0
  67. package/src/__tests__/selective-disclosure.test.ts +921 -0
  68. package/src/index.ts +260 -0
  69. package/src/linking/cardano-anchor-link.ts +682 -0
  70. package/src/linking/index.ts +58 -0
  71. package/src/linking/proof-publication.ts +557 -0
  72. package/src/midnight/index.ts +73 -0
  73. package/src/midnight/proof-server-client.ts +595 -0
  74. package/src/midnight/public-inputs.ts +590 -0
  75. package/src/midnight/witness-builder.ts +341 -0
  76. package/src/proofs/hash-chain-proof.ts +610 -0
  77. package/src/proofs/index.ts +77 -0
  78. package/src/proofs/policy-compliance-proof.ts +717 -0
  79. package/src/proofs/selective-disclosure.ts +839 -0
  80. package/src/prover-interface.ts +410 -0
  81. package/src/prover.ts +537 -0
  82. package/src/types.ts +551 -0
@@ -0,0 +1,839 @@
1
+ /**
2
+ * @fileoverview Selective disclosure proof generation and verification.
3
+ *
4
+ * Location: packages/midnight-prover/src/proofs/selective-disclosure.ts
5
+ *
6
+ * Summary:
7
+ * This module implements the SelectiveDisclosureProver class which generates ZK proofs
8
+ * demonstrating that a specific span exists in a trace bundle without revealing other
9
+ * spans. It uses Merkle proofs for inclusion verification and supports optional
10
+ * disclosure of the span and event data.
11
+ *
12
+ * Usage:
13
+ * - Used by the MidnightProver to generate selective disclosure proofs
14
+ * - Integrates with process-trace Merkle tree utilities
15
+ * - Binds proofs to Cardano anchor transactions for cross-chain verification
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { SelectiveDisclosureProver } from './selective-disclosure.js';
20
+ *
21
+ * const prover = new SelectiveDisclosureProver();
22
+ *
23
+ * const proof = await prover.generateProof({
24
+ * bundle: traceBundle,
25
+ * spanId: 'span-123',
26
+ * merkleRoot: traceBundle.merkleRoot,
27
+ * cardanoAnchorTxHash: 'txhash...',
28
+ * });
29
+ *
30
+ * const isValid = await prover.verifyProof(proof);
31
+ * ```
32
+ */
33
+
34
+ import {
35
+ sha256StringHex,
36
+ canonicalize,
37
+ } from "@fluxpointstudios/orynq-sdk-core/utils";
38
+
39
+ import type {
40
+ TraceBundle,
41
+ TraceSpan,
42
+ TraceEvent,
43
+ MerkleProof,
44
+ } from "@fluxpointstudios/orynq-sdk-process-trace";
45
+
46
+ import type {
47
+ DisclosureInput,
48
+ DisclosureProof,
49
+ DisclosurePublicInputs,
50
+ } from "../types.js";
51
+
52
+ import {
53
+ MidnightProverError,
54
+ MidnightProverException,
55
+ } from "../types.js";
56
+
57
+ // =============================================================================
58
+ // TYPES
59
+ // =============================================================================
60
+
61
+ /**
62
+ * Options for the SelectiveDisclosureProver.
63
+ */
64
+ export interface SelectiveDisclosureProverOptions {
65
+ /**
66
+ * Enable debug logging.
67
+ */
68
+ debug?: boolean;
69
+
70
+ /**
71
+ * Always include disclosed span data in the proof.
72
+ * Default: true
73
+ */
74
+ includeSpanData?: boolean;
75
+
76
+ /**
77
+ * Always include disclosed event data in the proof.
78
+ * Default: true
79
+ */
80
+ includeEventData?: boolean;
81
+ }
82
+
83
+ /**
84
+ * Merkle inclusion proof with span details.
85
+ */
86
+ export interface MerkleInclusionResult {
87
+ spanHash: string;
88
+ leafHash: string;
89
+ merkleProof: MerkleProof;
90
+ span: TraceSpan;
91
+ events: TraceEvent[];
92
+ }
93
+
94
+ /**
95
+ * Domain separation prefixes for selective disclosure proofs.
96
+ */
97
+ const DISCLOSURE_DOMAIN_PREFIXES = {
98
+ proof: "poi-prover:disclosure:v1|",
99
+ witness: "poi-prover:disclosure-witness:v1|",
100
+ publicInput: "poi-prover:disclosure-input:v1|",
101
+ span: "poi-trace:span:v1|",
102
+ leaf: "poi-trace:leaf:v1|",
103
+ node: "poi-trace:node:v1|",
104
+ } as const;
105
+
106
+ // =============================================================================
107
+ // MERKLE UTILITIES
108
+ // =============================================================================
109
+
110
+ /**
111
+ * Compute the hash for a span including its event hashes.
112
+ *
113
+ * The span hash is computed as:
114
+ * H("poi-trace:span:v1|" + canon(spanHeaderWithoutHash) + "|" + eventHashes joined by "|")
115
+ *
116
+ * @param span - The span to compute hash for
117
+ * @param eventHashes - Array of event hashes in sequence order
118
+ * @returns Promise resolving to the span hash as a hex string
119
+ */
120
+ export async function computeSpanHash(
121
+ span: TraceSpan,
122
+ eventHashes: string[]
123
+ ): Promise<string> {
124
+ // Extract span header (all fields except hash)
125
+ const spanHeader: Omit<TraceSpan, "hash"> = {
126
+ id: span.id,
127
+ spanSeq: span.spanSeq,
128
+ name: span.name,
129
+ status: span.status,
130
+ visibility: span.visibility,
131
+ startedAt: span.startedAt,
132
+ eventIds: span.eventIds,
133
+ childSpanIds: span.childSpanIds,
134
+ };
135
+
136
+ // Include optional fields only if they exist
137
+ if (span.parentSpanId !== undefined) {
138
+ (spanHeader as Record<string, unknown>).parentSpanId = span.parentSpanId;
139
+ }
140
+ if (span.endedAt !== undefined) {
141
+ (spanHeader as Record<string, unknown>).endedAt = span.endedAt;
142
+ }
143
+ if (span.durationMs !== undefined) {
144
+ (spanHeader as Record<string, unknown>).durationMs = span.durationMs;
145
+ }
146
+ if (span.metadata !== undefined) {
147
+ (spanHeader as Record<string, unknown>).metadata = span.metadata;
148
+ }
149
+
150
+ // Canonicalize the span header
151
+ const canonicalHeader = canonicalize(spanHeader, { removeNulls: true });
152
+
153
+ // Build the hash input: prefix + canon(header) + "|" + eventHashes joined by "|"
154
+ let hashInput = DISCLOSURE_DOMAIN_PREFIXES.span + canonicalHeader;
155
+
156
+ // Append event hashes if any exist
157
+ if (eventHashes.length > 0) {
158
+ hashInput += "|" + eventHashes.join("|");
159
+ }
160
+
161
+ return sha256StringHex(hashInput);
162
+ }
163
+
164
+ /**
165
+ * Compute the leaf hash for a span.
166
+ *
167
+ * The leaf hash is computed as: H("poi-trace:leaf:v1|" + spanHash)
168
+ *
169
+ * @param spanHash - The span hash
170
+ * @returns Promise resolving to the leaf hash
171
+ */
172
+ export async function computeLeafHash(spanHash: string): Promise<string> {
173
+ return sha256StringHex(DISCLOSURE_DOMAIN_PREFIXES.leaf + spanHash);
174
+ }
175
+
176
+ /**
177
+ * Compute the parent node hash from two children.
178
+ *
179
+ * The node hash is computed as: H("poi-trace:node:v1|" + left + "|" + right)
180
+ *
181
+ * @param left - Left child hash
182
+ * @param right - Right child hash
183
+ * @returns Promise resolving to the parent node hash
184
+ */
185
+ export async function computeNodeHash(left: string, right: string): Promise<string> {
186
+ return sha256StringHex(DISCLOSURE_DOMAIN_PREFIXES.node + left + "|" + right);
187
+ }
188
+
189
+ /**
190
+ * Generate a Merkle inclusion proof for a span in a bundle.
191
+ *
192
+ * @param bundle - The trace bundle containing the span
193
+ * @param spanId - ID of the span to prove inclusion for
194
+ * @returns MerkleInclusionResult with proof and span data
195
+ * @throws MidnightProverException if span not found
196
+ */
197
+ export async function generateMerkleInclusionProof(
198
+ bundle: TraceBundle,
199
+ spanId: string
200
+ ): Promise<MerkleInclusionResult> {
201
+ const { privateRun } = bundle;
202
+ const spans = privateRun.spans;
203
+ const events = privateRun.events;
204
+
205
+ // Find the span
206
+ const span = spans.find((s) => s.id === spanId);
207
+ if (!span) {
208
+ throw new MidnightProverException(
209
+ MidnightProverError.SPAN_NOT_FOUND,
210
+ `Span not found: ${spanId}`
211
+ );
212
+ }
213
+
214
+ // Create event map for quick lookup
215
+ const eventMap = new Map<string, TraceEvent>();
216
+ for (const event of events) {
217
+ eventMap.set(event.id, event);
218
+ }
219
+
220
+ // Get events for this span in seq order
221
+ const spanEvents = span.eventIds
222
+ .map((id) => eventMap.get(id))
223
+ .filter((e): e is TraceEvent => e !== undefined)
224
+ .sort((a, b) => a.seq - b.seq);
225
+
226
+ const eventHashes = spanEvents.map((e) => e.hash ?? "");
227
+
228
+ // Compute span hash
229
+ const spanHash = await computeSpanHash(span, eventHashes);
230
+
231
+ // Sort spans by spanSeq for consistent ordering
232
+ const sortedSpans = [...spans].sort((a, b) => a.spanSeq - b.spanSeq);
233
+ const spanIndex = sortedSpans.findIndex((s) => s.id === spanId);
234
+
235
+ if (spanIndex === -1) {
236
+ throw new MidnightProverException(
237
+ MidnightProverError.SPAN_NOT_FOUND,
238
+ `Span not found in sorted spans: ${spanId}`
239
+ );
240
+ }
241
+
242
+ // Build leaf hashes for all spans
243
+ const leafHashes: string[] = [];
244
+ for (const s of sortedSpans) {
245
+ const sEvents = s.eventIds
246
+ .map((id) => eventMap.get(id))
247
+ .filter((e): e is TraceEvent => e !== undefined)
248
+ .sort((a, b) => a.seq - b.seq);
249
+ const sEventHashes = sEvents.map((e) => e.hash ?? "");
250
+ const sSpanHash = await computeSpanHash(s, sEventHashes);
251
+ const sLeafHash = await computeLeafHash(sSpanHash);
252
+ leafHashes.push(sLeafHash);
253
+ }
254
+
255
+ // Compute the leaf hash for the target span
256
+ const leafHash = await computeLeafHash(spanHash);
257
+
258
+ // Generate Merkle proof (sibling path from leaf to root)
259
+ const siblings: Array<{ hash: string; position: "left" | "right" }> = [];
260
+
261
+ if (leafHashes.length > 1) {
262
+ let currentLevel = [...leafHashes];
263
+ let currentIndex = spanIndex;
264
+
265
+ while (currentLevel.length > 1) {
266
+ const isLeftChild = currentIndex % 2 === 0;
267
+ const siblingIndex = isLeftChild ? currentIndex + 1 : currentIndex - 1;
268
+
269
+ // Handle odd-leaf case: if no sibling exists, duplicate current
270
+ let siblingHash: string;
271
+ if (siblingIndex >= currentLevel.length) {
272
+ const currentHash = currentLevel[currentIndex];
273
+ if (!currentHash) {
274
+ throw new MidnightProverException(
275
+ MidnightProverError.PROOF_GENERATION_FAILED,
276
+ "Unexpected undefined hash at current index"
277
+ );
278
+ }
279
+ siblingHash = currentHash;
280
+ } else {
281
+ const hash = currentLevel[siblingIndex];
282
+ if (!hash) {
283
+ throw new MidnightProverException(
284
+ MidnightProverError.PROOF_GENERATION_FAILED,
285
+ "Unexpected undefined hash at sibling index"
286
+ );
287
+ }
288
+ siblingHash = hash;
289
+ }
290
+
291
+ siblings.push({
292
+ hash: siblingHash,
293
+ position: isLeftChild ? "right" : "left",
294
+ });
295
+
296
+ // Build next level
297
+ const nextLevel: string[] = [];
298
+ for (let i = 0; i < currentLevel.length; i += 2) {
299
+ const left = currentLevel[i] ?? "";
300
+ const right = i + 1 < currentLevel.length ? (currentLevel[i + 1] ?? left) : left;
301
+ const parentHash = await computeNodeHash(left, right);
302
+ nextLevel.push(parentHash);
303
+ }
304
+
305
+ currentLevel = nextLevel;
306
+ currentIndex = Math.floor(currentIndex / 2);
307
+ }
308
+ }
309
+
310
+ // Compute root hash for verification
311
+ const rootHash = leafHashes.length === 1 ? leafHashes[0] : await computeMerkleRoot(leafHashes);
312
+
313
+ if (!rootHash) {
314
+ throw new MidnightProverException(
315
+ MidnightProverError.PROOF_GENERATION_FAILED,
316
+ "Failed to compute Merkle root"
317
+ );
318
+ }
319
+
320
+ const merkleProof: MerkleProof = {
321
+ leafHash,
322
+ leafIndex: spanIndex,
323
+ siblings,
324
+ rootHash,
325
+ };
326
+
327
+ return {
328
+ spanHash,
329
+ leafHash,
330
+ merkleProof,
331
+ span,
332
+ events: spanEvents,
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Compute the Merkle root from leaf hashes.
338
+ *
339
+ * @param leafHashes - Array of leaf hashes
340
+ * @returns Promise resolving to the Merkle root hash
341
+ */
342
+ export async function computeMerkleRoot(leafHashes: string[]): Promise<string> {
343
+ if (leafHashes.length === 0) {
344
+ return "";
345
+ }
346
+
347
+ if (leafHashes.length === 1) {
348
+ return leafHashes[0] ?? "";
349
+ }
350
+
351
+ let currentLevel = [...leafHashes];
352
+
353
+ while (currentLevel.length > 1) {
354
+ const nextLevel: string[] = [];
355
+
356
+ for (let i = 0; i < currentLevel.length; i += 2) {
357
+ const left = currentLevel[i] ?? "";
358
+ const right = i + 1 < currentLevel.length ? (currentLevel[i + 1] ?? left) : left;
359
+ const parentHash = await computeNodeHash(left, right);
360
+ nextLevel.push(parentHash);
361
+ }
362
+
363
+ currentLevel = nextLevel;
364
+ }
365
+
366
+ return currentLevel[0] ?? "";
367
+ }
368
+
369
+ /**
370
+ * Verify a Merkle inclusion proof.
371
+ *
372
+ * @param proof - The Merkle proof to verify
373
+ * @returns Promise resolving to true if valid
374
+ */
375
+ export async function verifyMerkleProof(proof: MerkleProof): Promise<boolean> {
376
+ let currentHash = proof.leafHash;
377
+
378
+ for (const sibling of proof.siblings) {
379
+ let left: string;
380
+ let right: string;
381
+
382
+ if (sibling.position === "left") {
383
+ left = sibling.hash;
384
+ right = currentHash;
385
+ } else {
386
+ left = currentHash;
387
+ right = sibling.hash;
388
+ }
389
+
390
+ currentHash = await computeNodeHash(left, right);
391
+ }
392
+
393
+ return currentHash === proof.rootHash;
394
+ }
395
+
396
+ /**
397
+ * Verify span inclusion using the proof and span data.
398
+ *
399
+ * @param proof - The Merkle proof
400
+ * @param span - The span data
401
+ * @param events - The events belonging to the span
402
+ * @returns Promise resolving to true if span is validly included
403
+ */
404
+ export async function verifySpanInclusion(
405
+ proof: MerkleProof,
406
+ span: TraceSpan,
407
+ events: TraceEvent[]
408
+ ): Promise<boolean> {
409
+ // Sort events by seq for deterministic order
410
+ const sortedEvents = [...events].sort((a, b) => a.seq - b.seq);
411
+ const eventHashes = sortedEvents.map((e) => e.hash ?? "");
412
+
413
+ // Recompute span hash
414
+ const spanHash = await computeSpanHash(span, eventHashes);
415
+
416
+ // Compute expected leaf hash
417
+ const computedLeafHash = await computeLeafHash(spanHash);
418
+
419
+ // Verify leaf hash matches
420
+ if (computedLeafHash !== proof.leafHash) {
421
+ return false;
422
+ }
423
+
424
+ // Verify Merkle proof
425
+ return verifyMerkleProof(proof);
426
+ }
427
+
428
+ // =============================================================================
429
+ // SELECTIVE DISCLOSURE PROVER
430
+ // =============================================================================
431
+
432
+ /**
433
+ * SelectiveDisclosureProver generates and verifies ZK proofs of span membership.
434
+ *
435
+ * This prover creates a proof that a specific span exists in a trace bundle
436
+ * without revealing other spans. The proof uses Merkle tree inclusion proofs
437
+ * and optionally includes the disclosed span and event data.
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const prover = new SelectiveDisclosureProver();
442
+ * const proof = await prover.generateProof(input);
443
+ *
444
+ * // Proof includes optional span data for verification
445
+ * console.log(proof.disclosedSpan?.name); // e.g., "inference"
446
+ * ```
447
+ */
448
+ export class SelectiveDisclosureProver {
449
+ private readonly debug: boolean;
450
+ private readonly includeSpanData: boolean;
451
+ private readonly includeEventData: boolean;
452
+
453
+ /**
454
+ * Create a new SelectiveDisclosureProver instance.
455
+ *
456
+ * @param options - Configuration options
457
+ */
458
+ constructor(options: SelectiveDisclosureProverOptions = {}) {
459
+ this.debug = options.debug ?? false;
460
+ this.includeSpanData = options.includeSpanData ?? true;
461
+ this.includeEventData = options.includeEventData ?? true;
462
+ }
463
+
464
+ /**
465
+ * Generate a selective disclosure proof.
466
+ *
467
+ * This method creates a ZK proof that a specific span exists in the
468
+ * trace bundle's Merkle tree. The proof binds to the Cardano anchor
469
+ * transaction for cross-chain verification.
470
+ *
471
+ * @param input - Selective disclosure input
472
+ * @returns Promise resolving to the disclosure proof
473
+ * @throws MidnightProverException on validation or generation failure
474
+ */
475
+ async generateProof(input: DisclosureInput): Promise<DisclosureProof> {
476
+ const startTime = performance.now();
477
+
478
+ // Validate input
479
+ this.validateInput(input);
480
+
481
+ if (this.debug) {
482
+ console.log("[SelectiveDisclosureProver] Generating proof for span:", input.spanId);
483
+ }
484
+
485
+ // Generate Merkle inclusion proof
486
+ const inclusionResult = await generateMerkleInclusionProof(input.bundle, input.spanId);
487
+
488
+ // Verify the computed root matches the expected root
489
+ if (inclusionResult.merkleProof.rootHash !== input.merkleRoot) {
490
+ throw new MidnightProverException(
491
+ MidnightProverError.HASH_MISMATCH,
492
+ `Computed Merkle root ${inclusionResult.merkleProof.rootHash} does not match expected ${input.merkleRoot}`
493
+ );
494
+ }
495
+
496
+ // Generate proof bytes (mock implementation)
497
+ const proofBytes = await this.generateProofBytes(input, inclusionResult);
498
+
499
+ const endTime = performance.now();
500
+ const provingTimeMs = Math.round(endTime - startTime);
501
+
502
+ // Construct public inputs
503
+ const publicInputs: DisclosurePublicInputs = {
504
+ spanHash: inclusionResult.spanHash,
505
+ merkleRoot: input.merkleRoot,
506
+ cardanoAnchorTxHash: input.cardanoAnchorTxHash,
507
+ };
508
+
509
+ // Generate proof ID
510
+ const proofId = await this.generateProofId(publicInputs);
511
+
512
+ const proof: DisclosureProof = {
513
+ proofType: "selective-disclosure",
514
+ proofId,
515
+ proof: proofBytes,
516
+ createdAt: new Date().toISOString(),
517
+ provingTimeMs,
518
+ proofSizeBytes: proofBytes.length,
519
+ publicInputs,
520
+ disclosedSpan: this.includeSpanData ? inclusionResult.span : undefined,
521
+ disclosedEvents: this.includeEventData ? inclusionResult.events : undefined,
522
+ };
523
+
524
+ if (this.debug) {
525
+ console.log("[SelectiveDisclosureProver] Proof generated:", {
526
+ proofId,
527
+ spanHash: inclusionResult.spanHash,
528
+ leafIndex: inclusionResult.merkleProof.leafIndex,
529
+ siblingCount: inclusionResult.merkleProof.siblings.length,
530
+ provingTimeMs,
531
+ });
532
+ }
533
+
534
+ return proof;
535
+ }
536
+
537
+ /**
538
+ * Verify a selective disclosure proof.
539
+ *
540
+ * This method verifies the cryptographic validity of the proof and
541
+ * optionally verifies that disclosed span data matches the proof.
542
+ *
543
+ * @param proof - The disclosure proof to verify
544
+ * @returns Promise resolving to true if valid
545
+ */
546
+ async verifyProof(proof: DisclosureProof): Promise<boolean> {
547
+ try {
548
+ // Validate proof structure
549
+ if (proof.proofType !== "selective-disclosure") {
550
+ return false;
551
+ }
552
+
553
+ if (!proof.proof || proof.proof.length === 0) {
554
+ return false;
555
+ }
556
+
557
+ // Validate public inputs
558
+ const { publicInputs } = proof;
559
+ if (!publicInputs.spanHash || publicInputs.spanHash.length !== 64) {
560
+ return false;
561
+ }
562
+ if (!publicInputs.merkleRoot || publicInputs.merkleRoot.length !== 64) {
563
+ return false;
564
+ }
565
+ if (!publicInputs.cardanoAnchorTxHash || publicInputs.cardanoAnchorTxHash.length === 0) {
566
+ return false;
567
+ }
568
+
569
+ // Verify proof ID matches public inputs
570
+ const expectedProofId = await this.generateProofId(publicInputs);
571
+ if (proof.proofId !== expectedProofId) {
572
+ return false;
573
+ }
574
+
575
+ // Extract Merkle proof from proof bytes and verify
576
+ const merkleProofValid = await this.verifyProofBytes(proof.proof, publicInputs);
577
+ if (!merkleProofValid) {
578
+ return false;
579
+ }
580
+
581
+ // If disclosed span data is provided, verify it matches the span hash
582
+ if (proof.disclosedSpan && proof.disclosedEvents) {
583
+ // Note: This simplified verification doesn't check full Merkle path
584
+ // In production, we'd extract and verify the full Merkle proof
585
+ const sortedEvents = [...proof.disclosedEvents].sort((a, b) => a.seq - b.seq);
586
+ const eventHashes = sortedEvents.map((e) => e.hash ?? "");
587
+ const recomputedSpanHash = await computeSpanHash(proof.disclosedSpan, eventHashes);
588
+
589
+ if (recomputedSpanHash !== publicInputs.spanHash) {
590
+ return false;
591
+ }
592
+ }
593
+
594
+ return true;
595
+ } catch (error) {
596
+ if (this.debug) {
597
+ console.error("[SelectiveDisclosureProver] Verification error:", error);
598
+ }
599
+ return false;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Generate a proof without disclosure (membership proof only).
605
+ *
606
+ * This creates a proof that demonstrates span existence without
607
+ * revealing any span or event data.
608
+ *
609
+ * @param input - Selective disclosure input
610
+ * @returns Promise resolving to the disclosure proof without span data
611
+ */
612
+ async generateMembershipProof(input: DisclosureInput): Promise<DisclosureProof> {
613
+ // Create a new prover instance with disclosure disabled
614
+ const membershipProver = new SelectiveDisclosureProver({
615
+ debug: this.debug,
616
+ includeSpanData: false,
617
+ includeEventData: false,
618
+ });
619
+
620
+ return membershipProver.generateProof(input);
621
+ }
622
+
623
+ // ===========================================================================
624
+ // PRIVATE METHODS
625
+ // ===========================================================================
626
+
627
+ /**
628
+ * Validate disclosure input.
629
+ */
630
+ private validateInput(input: DisclosureInput): void {
631
+ if (!input.bundle) {
632
+ throw new MidnightProverException(
633
+ MidnightProverError.MISSING_REQUIRED_FIELD,
634
+ "Trace bundle is required"
635
+ );
636
+ }
637
+
638
+ if (!input.spanId || input.spanId.length === 0) {
639
+ throw new MidnightProverException(
640
+ MidnightProverError.MISSING_REQUIRED_FIELD,
641
+ "Span ID is required"
642
+ );
643
+ }
644
+
645
+ if (!input.merkleRoot || input.merkleRoot.length !== 64) {
646
+ throw new MidnightProverException(
647
+ MidnightProverError.INVALID_INPUT,
648
+ "Invalid Merkle root: must be 64-character hex string"
649
+ );
650
+ }
651
+
652
+ if (!input.cardanoAnchorTxHash || input.cardanoAnchorTxHash.length === 0) {
653
+ throw new MidnightProverException(
654
+ MidnightProverError.MISSING_REQUIRED_FIELD,
655
+ "Cardano anchor transaction hash is required"
656
+ );
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Generate mock proof bytes.
662
+ * In a real implementation, this would call the Midnight proof server.
663
+ */
664
+ private async generateProofBytes(
665
+ input: DisclosureInput,
666
+ inclusion: MerkleInclusionResult
667
+ ): Promise<Uint8Array> {
668
+ // Construct witness data (private inputs)
669
+ const witnessData = canonicalize({
670
+ spanId: input.spanId,
671
+ spanHash: inclusion.spanHash,
672
+ leafHash: inclusion.leafHash,
673
+ leafIndex: inclusion.merkleProof.leafIndex,
674
+ siblings: inclusion.merkleProof.siblings,
675
+ merkleRoot: input.merkleRoot,
676
+ cardanoAnchorTxHash: input.cardanoAnchorTxHash,
677
+ });
678
+
679
+ // Generate witness hash
680
+ const witnessHash = await sha256StringHex(DISCLOSURE_DOMAIN_PREFIXES.witness + witnessData);
681
+
682
+ // Construct public input commitment
683
+ const publicInputData = canonicalize({
684
+ spanHash: inclusion.spanHash,
685
+ merkleRoot: input.merkleRoot,
686
+ cardanoAnchorTxHash: input.cardanoAnchorTxHash,
687
+ });
688
+ const publicInputHash = await sha256StringHex(
689
+ DISCLOSURE_DOMAIN_PREFIXES.publicInput + publicInputData
690
+ );
691
+
692
+ // Generate mock proof: commitment to witness + public inputs
693
+ const proofCommitment = await sha256StringHex(
694
+ DISCLOSURE_DOMAIN_PREFIXES.proof + witnessHash + "|" + publicInputHash
695
+ );
696
+
697
+ // Encode Merkle proof siblings for inclusion in proof bytes
698
+ const siblingData = inclusion.merkleProof.siblings.map((s) => ({
699
+ hash: s.hash,
700
+ pos: s.position === "left" ? 0 : 1,
701
+ }));
702
+ const siblingJson = JSON.stringify(siblingData);
703
+ const siblingBytes = new TextEncoder().encode(siblingJson);
704
+
705
+ // Construct proof bytes:
706
+ // - version (1 byte)
707
+ // - commitment (32 bytes)
708
+ // - witness hash (32 bytes)
709
+ // - sibling count (2 bytes)
710
+ // - sibling data (variable)
711
+ const proofBytes = new Uint8Array(67 + siblingBytes.length);
712
+ proofBytes[0] = 0x01; // Version 1
713
+
714
+ // Copy commitment hash
715
+ const commitmentBytes = this.hexToBytes(proofCommitment);
716
+ proofBytes.set(commitmentBytes, 1);
717
+
718
+ // Copy witness hash
719
+ const witnessBytes = this.hexToBytes(witnessHash);
720
+ proofBytes.set(witnessBytes, 33);
721
+
722
+ // Write sibling count (big-endian)
723
+ const siblingCount = inclusion.merkleProof.siblings.length;
724
+ proofBytes[65] = (siblingCount >> 8) & 0xff;
725
+ proofBytes[66] = siblingCount & 0xff;
726
+
727
+ // Copy sibling data
728
+ proofBytes.set(siblingBytes, 67);
729
+
730
+ return proofBytes;
731
+ }
732
+
733
+ /**
734
+ * Verify mock proof bytes.
735
+ */
736
+ private async verifyProofBytes(
737
+ proofBytes: Uint8Array,
738
+ publicInputs: DisclosurePublicInputs
739
+ ): Promise<boolean> {
740
+ // Check minimum proof size
741
+ if (proofBytes.length < 67) {
742
+ return false;
743
+ }
744
+
745
+ // Check version byte
746
+ if (proofBytes[0] !== 0x01) {
747
+ return false;
748
+ }
749
+
750
+ // Extract and verify commitment is non-zero
751
+ const commitmentBytes = proofBytes.slice(1, 33);
752
+ const witnessHashBytes = proofBytes.slice(33, 65);
753
+
754
+ const commitmentNonZero = commitmentBytes.some((b) => b !== 0);
755
+ const witnessNonZero = witnessHashBytes.some((b) => b !== 0);
756
+
757
+ if (!commitmentNonZero || !witnessNonZero) {
758
+ return false;
759
+ }
760
+
761
+ // Extract sibling count
762
+ const siblingCount = (proofBytes[65]! << 8) | proofBytes[66]!;
763
+
764
+ // Extract and parse sibling data if present
765
+ if (siblingCount > 0 && proofBytes.length > 67) {
766
+ try {
767
+ const siblingJson = new TextDecoder().decode(proofBytes.slice(67));
768
+ const siblings = JSON.parse(siblingJson) as Array<{ hash: string; pos: number }>;
769
+
770
+ if (siblings.length !== siblingCount) {
771
+ return false;
772
+ }
773
+
774
+ // Reconstruct Merkle proof and verify
775
+ const merkleProof: MerkleProof = {
776
+ leafHash: await computeLeafHash(publicInputs.spanHash),
777
+ leafIndex: 0, // Not needed for verification
778
+ siblings: siblings.map((s) => ({
779
+ hash: s.hash,
780
+ position: s.pos === 0 ? "left" : "right",
781
+ })),
782
+ rootHash: publicInputs.merkleRoot,
783
+ };
784
+
785
+ return verifyMerkleProof(merkleProof);
786
+ } catch {
787
+ // JSON parse error or other issue
788
+ return false;
789
+ }
790
+ }
791
+
792
+ // For single-leaf trees (no siblings), verify leaf hash equals root
793
+ if (siblingCount === 0) {
794
+ const leafHash = await computeLeafHash(publicInputs.spanHash);
795
+ return leafHash === publicInputs.merkleRoot;
796
+ }
797
+
798
+ return true;
799
+ }
800
+
801
+ /**
802
+ * Generate proof ID from public inputs.
803
+ */
804
+ private async generateProofId(publicInputs: DisclosurePublicInputs): Promise<string> {
805
+ const data = canonicalize({
806
+ type: "selective-disclosure",
807
+ ...publicInputs,
808
+ });
809
+ const hash = await sha256StringHex(data);
810
+ return `disclosure-proof-${hash.slice(0, 16)}`;
811
+ }
812
+
813
+ /**
814
+ * Convert hex string to bytes.
815
+ */
816
+ private hexToBytes(hex: string): Uint8Array {
817
+ const bytes = new Uint8Array(hex.length / 2);
818
+ for (let i = 0; i < hex.length; i += 2) {
819
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
820
+ }
821
+ return bytes;
822
+ }
823
+ }
824
+
825
+ // =============================================================================
826
+ // FACTORY FUNCTION
827
+ // =============================================================================
828
+
829
+ /**
830
+ * Create a new SelectiveDisclosureProver instance.
831
+ *
832
+ * @param options - Configuration options
833
+ * @returns New prover instance
834
+ */
835
+ export function createSelectiveDisclosureProver(
836
+ options?: SelectiveDisclosureProverOptions
837
+ ): SelectiveDisclosureProver {
838
+ return new SelectiveDisclosureProver(options);
839
+ }