@sip-protocol/sdk 0.1.0 → 0.1.1

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.
@@ -4,7 +4,7 @@
4
4
  * Production-ready ZK proof provider using Noir (Aztec) circuits.
5
5
  *
6
6
  * This provider generates cryptographically sound proofs using:
7
- * - Funding Proof: ~22,000 constraints (docs/specs/FUNDING-PROOF.md)
7
+ * - Funding Proof: ~2,000 constraints (docs/specs/FUNDING-PROOF.md)
8
8
  * - Validity Proof: ~72,000 constraints (docs/specs/VALIDITY-PROOF.md)
9
9
  * - Fulfillment Proof: ~22,000 constraints (docs/specs/FULFILLMENT-PROOF.md)
10
10
  *
@@ -23,14 +23,28 @@ import type {
23
23
  import { ProofGenerationError } from './interface'
24
24
  import { ProofError, ErrorCode } from '../errors'
25
25
 
26
+ // Import Noir JS (dynamically loaded to support both Node and browser)
27
+ import { Noir } from '@noir-lang/noir_js'
28
+ import type { CompiledCircuit } from '@noir-lang/types'
29
+ import { UltraHonkBackend } from '@aztec/bb.js'
30
+ import { secp256k1 } from '@noble/curves/secp256k1'
31
+
32
+ // Import compiled circuit artifacts
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ import fundingCircuitArtifact from './circuits/funding_proof.json'
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ import validityCircuitArtifact from './circuits/validity_proof.json'
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ import fulfillmentCircuitArtifact from './circuits/fulfillment_proof.json'
39
+
26
40
  /**
27
- * Noir circuit artifacts paths
28
- * These will be populated when circuits are compiled (#14, #15, #16)
41
+ * Public key coordinates for secp256k1
29
42
  */
30
- interface NoirCircuitArtifacts {
31
- fundingCircuit?: unknown
32
- validityCircuit?: unknown
33
- fulfillmentCircuit?: unknown
43
+ export interface PublicKeyCoordinates {
44
+ /** X coordinate as 32-byte array */
45
+ x: number[]
46
+ /** Y coordinate as 32-byte array */
47
+ y: number[]
34
48
  }
35
49
 
36
50
  /**
@@ -45,7 +59,7 @@ export interface NoirProviderConfig {
45
59
 
46
60
  /**
47
61
  * Backend to use for proof generation
48
- * @default 'barretenberg' (UltraPlonk)
62
+ * @default 'barretenberg' (UltraHonk)
49
63
  */
50
64
  backend?: 'barretenberg'
51
65
 
@@ -54,6 +68,12 @@ export interface NoirProviderConfig {
54
68
  * @default false
55
69
  */
56
70
  verbose?: boolean
71
+
72
+ /**
73
+ * Oracle public key for verifying attestations in fulfillment proofs
74
+ * Required for production use. If not provided, proofs will use placeholder keys.
75
+ */
76
+ oraclePublicKey?: PublicKeyCoordinates
57
77
  }
58
78
 
59
79
  /**
@@ -63,16 +83,17 @@ export interface NoirProviderConfig {
63
83
  *
64
84
  * @example
65
85
  * ```typescript
66
- * const provider = new NoirProofProvider({
67
- * artifactsPath: './circuits/target',
68
- * })
86
+ * const provider = new NoirProofProvider()
69
87
  *
70
88
  * await provider.initialize()
71
89
  *
72
90
  * const result = await provider.generateFundingProof({
73
91
  * balance: 100n,
74
92
  * minimumRequired: 50n,
75
- * // ... other params
93
+ * blindingFactor: new Uint8Array(32),
94
+ * assetId: '0xABCD',
95
+ * userAddress: '0x1234...',
96
+ * ownershipSignature: new Uint8Array(64),
76
97
  * })
77
98
  * ```
78
99
  */
@@ -80,7 +101,14 @@ export class NoirProofProvider implements ProofProvider {
80
101
  readonly framework: ProofFramework = 'noir'
81
102
  private _isReady = false
82
103
  private config: NoirProviderConfig
83
- private artifacts: NoirCircuitArtifacts = {}
104
+
105
+ // Circuit instances
106
+ private fundingNoir: Noir | null = null
107
+ private fundingBackend: UltraHonkBackend | null = null
108
+ private validityNoir: Noir | null = null
109
+ private validityBackend: UltraHonkBackend | null = null
110
+ private fulfillmentNoir: Noir | null = null
111
+ private fulfillmentBackend: UltraHonkBackend | null = null
84
112
 
85
113
  constructor(config: NoirProviderConfig = {}) {
86
114
  this.config = {
@@ -94,130 +122,690 @@ export class NoirProofProvider implements ProofProvider {
94
122
  return this._isReady
95
123
  }
96
124
 
125
+ /**
126
+ * Derive secp256k1 public key coordinates from a private key
127
+ *
128
+ * Utility method that can be used to generate public key coordinates
129
+ * for use in ValidityProofParams.senderPublicKey or NoirProviderConfig.oraclePublicKey
130
+ *
131
+ * @param privateKey - 32-byte private key
132
+ * @returns X and Y coordinates as 32-byte arrays
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const privateKey = new Uint8Array(32).fill(1) // Your secret key
137
+ * const publicKey = NoirProofProvider.derivePublicKey(privateKey)
138
+ *
139
+ * // Use for oracle configuration
140
+ * const provider = new NoirProofProvider({
141
+ * oraclePublicKey: publicKey
142
+ * })
143
+ *
144
+ * // Or use for validity proof params
145
+ * const validityParams = {
146
+ * // ... other params
147
+ * senderPublicKey: {
148
+ * x: new Uint8Array(publicKey.x),
149
+ * y: new Uint8Array(publicKey.y)
150
+ * }
151
+ * }
152
+ * ```
153
+ */
154
+ static derivePublicKey(privateKey: Uint8Array): PublicKeyCoordinates {
155
+ // Get uncompressed public key (65 bytes: 04 || x || y)
156
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
157
+
158
+ // Extract X (bytes 1-32) and Y (bytes 33-64)
159
+ const x = Array.from(uncompressedPubKey.slice(1, 33))
160
+ const y = Array.from(uncompressedPubKey.slice(33, 65))
161
+
162
+ return { x, y }
163
+ }
164
+
97
165
  /**
98
166
  * Initialize the Noir provider
99
167
  *
100
168
  * Loads circuit artifacts and initializes the proving backend.
101
- *
102
- * @throws Error if circuits are not yet implemented
103
169
  */
104
170
  async initialize(): Promise<void> {
105
- // TODO: Implement when circuits are ready (#14, #15, #16)
106
- //
107
- // Implementation will:
108
- // 1. Load compiled circuit artifacts from artifactsPath
109
- // 2. Initialize Barretenberg backend
110
- // 3. Load proving/verification keys
111
- //
112
- // Dependencies:
113
- // - @noir-lang/noir_js
114
- // - @noir-lang/backend_barretenberg
115
- //
116
- // Example:
117
- // ```typescript
118
- // import { Noir } from '@noir-lang/noir_js'
119
- // import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'
120
- //
121
- // const circuit = await import('./circuits/funding/target/funding.json')
122
- // const backend = new BarretenbergBackend(circuit)
123
- // const noir = new Noir(circuit, backend)
124
- // ```
125
-
126
- throw new ProofError(
127
- 'NoirProofProvider not yet implemented. ' +
128
- 'Circuits must be compiled first. See issues #14, #15, #16.',
129
- ErrorCode.PROOF_NOT_IMPLEMENTED,
130
- { context: { issues: ['#14', '#15', '#16'] } }
131
- )
171
+ if (this._isReady) {
172
+ return
173
+ }
174
+
175
+ try {
176
+ if (this.config.verbose) {
177
+ console.log('[NoirProofProvider] Initializing...')
178
+ }
179
+
180
+ // Initialize Funding Proof circuit
181
+ // Cast to CompiledCircuit - the JSON artifact matches the expected structure
182
+ const fundingCircuit = fundingCircuitArtifact as unknown as CompiledCircuit
183
+
184
+ // Create backend for proof generation
185
+ this.fundingBackend = new UltraHonkBackend(fundingCircuit.bytecode)
186
+
187
+ // Create Noir instance for witness generation
188
+ this.fundingNoir = new Noir(fundingCircuit)
189
+
190
+ if (this.config.verbose) {
191
+ console.log('[NoirProofProvider] Funding circuit loaded')
192
+ // Access noir_version from the raw artifact since CompiledCircuit type may not include it
193
+ const artifactVersion = (fundingCircuitArtifact as { noir_version?: string }).noir_version
194
+ console.log(`[NoirProofProvider] Noir version: ${artifactVersion ?? 'unknown'}`)
195
+ }
196
+
197
+ // Initialize Validity Proof circuit
198
+ const validityCircuit = validityCircuitArtifact as unknown as CompiledCircuit
199
+
200
+ // Create backend for validity proof generation
201
+ this.validityBackend = new UltraHonkBackend(validityCircuit.bytecode)
202
+
203
+ // Create Noir instance for validity witness generation
204
+ this.validityNoir = new Noir(validityCircuit)
205
+
206
+ if (this.config.verbose) {
207
+ console.log('[NoirProofProvider] Validity circuit loaded')
208
+ }
209
+
210
+ // Initialize Fulfillment Proof circuit
211
+ const fulfillmentCircuit = fulfillmentCircuitArtifact as unknown as CompiledCircuit
212
+
213
+ // Create backend for fulfillment proof generation
214
+ this.fulfillmentBackend = new UltraHonkBackend(fulfillmentCircuit.bytecode)
215
+
216
+ // Create Noir instance for fulfillment witness generation
217
+ this.fulfillmentNoir = new Noir(fulfillmentCircuit)
218
+
219
+ if (this.config.verbose) {
220
+ console.log('[NoirProofProvider] Fulfillment circuit loaded')
221
+ }
222
+
223
+ this._isReady = true
224
+
225
+ if (this.config.verbose) {
226
+ console.log('[NoirProofProvider] Initialization complete')
227
+ }
228
+ } catch (error) {
229
+ throw new ProofError(
230
+ `Failed to initialize NoirProofProvider: ${error instanceof Error ? error.message : String(error)}`,
231
+ ErrorCode.PROOF_NOT_IMPLEMENTED,
232
+ { context: { error } }
233
+ )
234
+ }
132
235
  }
133
236
 
134
237
  /**
135
238
  * Generate a Funding Proof using Noir circuits
136
239
  *
240
+ * Proves: balance >= minimumRequired without revealing balance
241
+ *
137
242
  * @see docs/specs/FUNDING-PROOF.md
138
243
  */
139
- async generateFundingProof(_params: FundingProofParams): Promise<ProofResult> {
244
+ async generateFundingProof(params: FundingProofParams): Promise<ProofResult> {
140
245
  this.ensureReady()
141
246
 
142
- // TODO: Implement when circuit is ready (#14)
143
- //
144
- // Implementation will:
145
- // 1. Prepare witness inputs from params
146
- // 2. Execute circuit to generate proof
147
- // 3. Extract public inputs
148
- //
149
- // Example:
150
- // ```typescript
151
- // const witness = {
152
- // balance: params.balance,
153
- // minimum_required: params.minimumRequired,
154
- // blinding: params.blindingFactor,
155
- // // ... other inputs
156
- // }
157
- //
158
- // const proof = await this.fundingCircuit.generateProof(witness)
159
- // return { proof, publicInputs: proof.publicInputs }
160
- // ```
161
-
162
- throw new ProofGenerationError(
163
- 'funding',
164
- 'Noir circuit not yet implemented. See #14.',
165
- )
247
+ if (!this.fundingNoir || !this.fundingBackend) {
248
+ throw new ProofGenerationError(
249
+ 'funding',
250
+ 'Funding circuit not initialized'
251
+ )
252
+ }
253
+
254
+ try {
255
+ if (this.config.verbose) {
256
+ console.log('[NoirProofProvider] Generating funding proof...')
257
+ }
258
+
259
+ // Compute the commitment hash that the circuit expects
260
+ // The circuit computes: pedersen_hash([commitment.x, commitment.y, asset_id])
261
+ // We need to compute this to pass as a public input
262
+ const { commitmentHash, blindingField } = await this.computeCommitmentHash(
263
+ params.balance,
264
+ params.blindingFactor,
265
+ params.assetId
266
+ )
267
+
268
+ // Prepare witness inputs for the circuit
269
+ const witnessInputs = {
270
+ // Public inputs
271
+ commitment_hash: commitmentHash,
272
+ minimum_required: params.minimumRequired.toString(),
273
+ asset_id: this.assetIdToField(params.assetId),
274
+ // Private inputs
275
+ balance: params.balance.toString(),
276
+ blinding: blindingField,
277
+ }
278
+
279
+ if (this.config.verbose) {
280
+ console.log('[NoirProofProvider] Witness inputs:', {
281
+ commitment_hash: commitmentHash,
282
+ minimum_required: params.minimumRequired.toString(),
283
+ asset_id: this.assetIdToField(params.assetId),
284
+ balance: '[PRIVATE]',
285
+ blinding: '[PRIVATE]',
286
+ })
287
+ }
288
+
289
+ // Execute circuit to generate witness
290
+ const { witness } = await this.fundingNoir.execute(witnessInputs)
291
+
292
+ if (this.config.verbose) {
293
+ console.log('[NoirProofProvider] Witness generated, creating proof...')
294
+ }
295
+
296
+ // Generate proof using backend
297
+ const proofData = await this.fundingBackend.generateProof(witness)
298
+
299
+ if (this.config.verbose) {
300
+ console.log('[NoirProofProvider] Proof generated successfully')
301
+ }
302
+
303
+ // Extract public inputs from the proof
304
+ const publicInputs: `0x${string}`[] = [
305
+ `0x${commitmentHash}`,
306
+ `0x${params.minimumRequired.toString(16).padStart(16, '0')}`,
307
+ `0x${this.assetIdToField(params.assetId)}`,
308
+ ]
309
+
310
+ // Create ZKProof object
311
+ const proof: ZKProof = {
312
+ type: 'funding',
313
+ proof: `0x${Buffer.from(proofData.proof).toString('hex')}`,
314
+ publicInputs,
315
+ }
316
+
317
+ return {
318
+ proof,
319
+ publicInputs,
320
+ }
321
+ } catch (error) {
322
+ const message = error instanceof Error ? error.message : String(error)
323
+
324
+ // Check for specific circuit errors
325
+ if (message.includes('Insufficient balance')) {
326
+ throw new ProofGenerationError(
327
+ 'funding',
328
+ 'Insufficient balance to generate proof',
329
+ error instanceof Error ? error : undefined
330
+ )
331
+ }
332
+ if (message.includes('Commitment hash mismatch')) {
333
+ throw new ProofGenerationError(
334
+ 'funding',
335
+ 'Commitment hash verification failed',
336
+ error instanceof Error ? error : undefined
337
+ )
338
+ }
339
+
340
+ throw new ProofGenerationError(
341
+ 'funding',
342
+ `Failed to generate funding proof: ${message}`,
343
+ error instanceof Error ? error : undefined
344
+ )
345
+ }
166
346
  }
167
347
 
168
348
  /**
169
349
  * Generate a Validity Proof using Noir circuits
170
350
  *
351
+ * Proves: Intent is authorized by sender without revealing identity
352
+ *
171
353
  * @see docs/specs/VALIDITY-PROOF.md
172
354
  */
173
- async generateValidityProof(_params: ValidityProofParams): Promise<ProofResult> {
355
+ async generateValidityProof(params: ValidityProofParams): Promise<ProofResult> {
174
356
  this.ensureReady()
175
357
 
176
- // TODO: Implement when circuit is ready (#15)
177
- throw new ProofGenerationError(
178
- 'validity',
179
- 'Noir circuit not yet implemented. See #15.',
180
- )
358
+ if (!this.validityNoir || !this.validityBackend) {
359
+ throw new ProofGenerationError(
360
+ 'validity',
361
+ 'Validity circuit not initialized'
362
+ )
363
+ }
364
+
365
+ try {
366
+ if (this.config.verbose) {
367
+ console.log('[NoirProofProvider] Generating validity proof...')
368
+ }
369
+
370
+ // Convert intent hash to field
371
+ const intentHashField = this.hexToField(params.intentHash)
372
+
373
+ // Convert sender address to field
374
+ const senderAddressField = this.hexToField(params.senderAddress)
375
+
376
+ // Convert blinding to field
377
+ const senderBlindingField = this.bytesToField(params.senderBlinding)
378
+
379
+ // Convert sender secret to field
380
+ const senderSecretField = this.bytesToField(params.senderSecret)
381
+
382
+ // Convert nonce to field
383
+ const nonceField = this.bytesToField(params.nonce)
384
+
385
+ // Compute sender commitment (same as circuit will do)
386
+ const { commitmentX, commitmentY } = await this.computeSenderCommitment(
387
+ senderAddressField,
388
+ senderBlindingField
389
+ )
390
+
391
+ // Compute nullifier (same as circuit will do)
392
+ const nullifier = await this.computeNullifier(
393
+ senderSecretField,
394
+ intentHashField,
395
+ nonceField
396
+ )
397
+
398
+ // Extract public key components from signature (assuming 64-byte signature)
399
+ // For ECDSA, we need the public key separately
400
+ // The signature is r (32 bytes) + s (32 bytes)
401
+ const signature = Array.from(params.authorizationSignature)
402
+
403
+ // Create message hash from intent hash (32 bytes)
404
+ const messageHash = this.fieldToBytes32(intentHashField)
405
+
406
+ // Use provided public key or derive from sender's secret key
407
+ // The sender secret is used as the private key for ECDSA signature verification
408
+ let pubKeyX: number[]
409
+ let pubKeyY: number[]
410
+
411
+ if (params.senderPublicKey) {
412
+ // Use provided public key
413
+ pubKeyX = Array.from(params.senderPublicKey.x)
414
+ pubKeyY = Array.from(params.senderPublicKey.y)
415
+ } else {
416
+ // Derive from sender secret
417
+ const coords = this.getPublicKeyCoordinates(params.senderSecret)
418
+ pubKeyX = coords.x
419
+ pubKeyY = coords.y
420
+ }
421
+
422
+ // Prepare witness inputs for the circuit
423
+ const witnessInputs = {
424
+ // Public inputs
425
+ intent_hash: intentHashField,
426
+ sender_commitment_x: commitmentX,
427
+ sender_commitment_y: commitmentY,
428
+ nullifier: nullifier,
429
+ timestamp: params.timestamp.toString(),
430
+ expiry: params.expiry.toString(),
431
+ // Private inputs
432
+ sender_address: senderAddressField,
433
+ sender_blinding: senderBlindingField,
434
+ sender_secret: senderSecretField,
435
+ pub_key_x: pubKeyX,
436
+ pub_key_y: pubKeyY,
437
+ signature: signature,
438
+ message_hash: messageHash,
439
+ nonce: nonceField,
440
+ }
441
+
442
+ if (this.config.verbose) {
443
+ console.log('[NoirProofProvider] Validity witness inputs:', {
444
+ intent_hash: intentHashField,
445
+ sender_commitment_x: commitmentX,
446
+ sender_commitment_y: commitmentY,
447
+ nullifier: nullifier,
448
+ timestamp: params.timestamp,
449
+ expiry: params.expiry,
450
+ sender_address: '[PRIVATE]',
451
+ sender_blinding: '[PRIVATE]',
452
+ sender_secret: '[PRIVATE]',
453
+ signature: '[PRIVATE]',
454
+ })
455
+ }
456
+
457
+ // Execute circuit to generate witness
458
+ const { witness } = await this.validityNoir.execute(witnessInputs)
459
+
460
+ if (this.config.verbose) {
461
+ console.log('[NoirProofProvider] Validity witness generated, creating proof...')
462
+ }
463
+
464
+ // Generate proof using backend
465
+ const proofData = await this.validityBackend.generateProof(witness)
466
+
467
+ if (this.config.verbose) {
468
+ console.log('[NoirProofProvider] Validity proof generated successfully')
469
+ }
470
+
471
+ // Extract public inputs from the proof
472
+ const publicInputs: `0x${string}`[] = [
473
+ `0x${intentHashField}`,
474
+ `0x${commitmentX}`,
475
+ `0x${commitmentY}`,
476
+ `0x${nullifier}`,
477
+ `0x${params.timestamp.toString(16).padStart(16, '0')}`,
478
+ `0x${params.expiry.toString(16).padStart(16, '0')}`,
479
+ ]
480
+
481
+ // Create ZKProof object
482
+ const proof: ZKProof = {
483
+ type: 'validity',
484
+ proof: `0x${Buffer.from(proofData.proof).toString('hex')}`,
485
+ publicInputs,
486
+ }
487
+
488
+ return {
489
+ proof,
490
+ publicInputs,
491
+ }
492
+ } catch (error) {
493
+ const message = error instanceof Error ? error.message : String(error)
494
+
495
+ // Check for specific circuit errors
496
+ if (message.includes('Sender commitment')) {
497
+ throw new ProofGenerationError(
498
+ 'validity',
499
+ 'Sender commitment verification failed',
500
+ error instanceof Error ? error : undefined
501
+ )
502
+ }
503
+ if (message.includes('Invalid ECDSA')) {
504
+ throw new ProofGenerationError(
505
+ 'validity',
506
+ 'Authorization signature verification failed',
507
+ error instanceof Error ? error : undefined
508
+ )
509
+ }
510
+ if (message.includes('Nullifier mismatch')) {
511
+ throw new ProofGenerationError(
512
+ 'validity',
513
+ 'Nullifier derivation failed',
514
+ error instanceof Error ? error : undefined
515
+ )
516
+ }
517
+ if (message.includes('Intent expired')) {
518
+ throw new ProofGenerationError(
519
+ 'validity',
520
+ 'Intent has expired (timestamp >= expiry)',
521
+ error instanceof Error ? error : undefined
522
+ )
523
+ }
524
+
525
+ throw new ProofGenerationError(
526
+ 'validity',
527
+ `Failed to generate validity proof: ${message}`,
528
+ error instanceof Error ? error : undefined
529
+ )
530
+ }
181
531
  }
182
532
 
183
533
  /**
184
534
  * Generate a Fulfillment Proof using Noir circuits
185
535
  *
536
+ * Proves: Solver correctly executed the intent and delivered the required
537
+ * output to the recipient, without revealing execution path or liquidity sources.
538
+ *
186
539
  * @see docs/specs/FULFILLMENT-PROOF.md
187
540
  */
188
- async generateFulfillmentProof(_params: FulfillmentProofParams): Promise<ProofResult> {
541
+ async generateFulfillmentProof(params: FulfillmentProofParams): Promise<ProofResult> {
189
542
  this.ensureReady()
190
543
 
191
- // TODO: Implement when circuit is ready (#16)
192
- throw new ProofGenerationError(
193
- 'fulfillment',
194
- 'Noir circuit not yet implemented. See #16.',
195
- )
544
+ if (!this.fulfillmentNoir || !this.fulfillmentBackend) {
545
+ throw new ProofGenerationError(
546
+ 'fulfillment',
547
+ 'Fulfillment circuit not initialized'
548
+ )
549
+ }
550
+
551
+ try {
552
+ if (this.config.verbose) {
553
+ console.log('[NoirProofProvider] Generating fulfillment proof...')
554
+ }
555
+
556
+ // Convert intent hash to field
557
+ const intentHashField = this.hexToField(params.intentHash)
558
+
559
+ // Convert recipient stealth to field
560
+ const recipientStealthField = this.hexToField(params.recipientStealth)
561
+
562
+ // Compute output commitment
563
+ const { commitmentX, commitmentY } = await this.computeOutputCommitment(
564
+ params.outputAmount,
565
+ params.outputBlinding
566
+ )
567
+
568
+ // Compute solver ID from secret
569
+ const solverSecretField = this.bytesToField(params.solverSecret)
570
+ const solverId = await this.computeSolverId(solverSecretField)
571
+
572
+ // Convert output blinding to field
573
+ const outputBlindingField = this.bytesToField(params.outputBlinding)
574
+
575
+ // Oracle attestation data
576
+ const attestation = params.oracleAttestation
577
+ const attestationRecipientField = this.hexToField(attestation.recipient)
578
+ const attestationTxHashField = this.hexToField(attestation.txHash)
579
+
580
+ // Oracle signature (64 bytes)
581
+ const oracleSignature = Array.from(attestation.signature)
582
+
583
+ // Compute oracle message hash
584
+ const oracleMessageHash = await this.computeOracleMessageHash(
585
+ attestation.recipient,
586
+ attestation.amount,
587
+ attestation.txHash,
588
+ attestation.blockNumber
589
+ )
590
+
591
+ // Use configured oracle public key, or placeholder if not configured
592
+ // In production, the oracle public key should always be configured
593
+ const oraclePubKeyX = this.config.oraclePublicKey?.x ?? new Array(32).fill(0)
594
+ const oraclePubKeyY = this.config.oraclePublicKey?.y ?? new Array(32).fill(0)
595
+
596
+ if (!this.config.oraclePublicKey && this.config.verbose) {
597
+ console.warn('[NoirProofProvider] Warning: No oracle public key configured. Using placeholder keys.')
598
+ }
599
+
600
+ // Prepare witness inputs for the circuit
601
+ const witnessInputs = {
602
+ // Public inputs
603
+ intent_hash: intentHashField,
604
+ output_commitment_x: commitmentX,
605
+ output_commitment_y: commitmentY,
606
+ recipient_stealth: recipientStealthField,
607
+ min_output_amount: params.minOutputAmount.toString(),
608
+ solver_id: solverId,
609
+ fulfillment_time: params.fulfillmentTime.toString(),
610
+ expiry: params.expiry.toString(),
611
+ // Private inputs
612
+ output_amount: params.outputAmount.toString(),
613
+ output_blinding: outputBlindingField,
614
+ solver_secret: solverSecretField,
615
+ attestation_recipient: attestationRecipientField,
616
+ attestation_amount: attestation.amount.toString(),
617
+ attestation_tx_hash: attestationTxHashField,
618
+ attestation_block: attestation.blockNumber.toString(),
619
+ oracle_signature: oracleSignature,
620
+ oracle_message_hash: oracleMessageHash,
621
+ oracle_pub_key_x: oraclePubKeyX,
622
+ oracle_pub_key_y: oraclePubKeyY,
623
+ }
624
+
625
+ if (this.config.verbose) {
626
+ console.log('[NoirProofProvider] Fulfillment witness inputs:', {
627
+ intent_hash: intentHashField,
628
+ output_commitment_x: commitmentX,
629
+ output_commitment_y: commitmentY,
630
+ recipient_stealth: recipientStealthField,
631
+ min_output_amount: params.minOutputAmount.toString(),
632
+ solver_id: solverId,
633
+ fulfillment_time: params.fulfillmentTime,
634
+ expiry: params.expiry,
635
+ output_amount: '[PRIVATE]',
636
+ output_blinding: '[PRIVATE]',
637
+ solver_secret: '[PRIVATE]',
638
+ oracle_attestation: '[PRIVATE]',
639
+ })
640
+ }
641
+
642
+ // Execute circuit to generate witness
643
+ const { witness } = await this.fulfillmentNoir.execute(witnessInputs)
644
+
645
+ if (this.config.verbose) {
646
+ console.log('[NoirProofProvider] Fulfillment witness generated, creating proof...')
647
+ }
648
+
649
+ // Generate proof using backend
650
+ const proofData = await this.fulfillmentBackend.generateProof(witness)
651
+
652
+ if (this.config.verbose) {
653
+ console.log('[NoirProofProvider] Fulfillment proof generated successfully')
654
+ }
655
+
656
+ // Extract public inputs from the proof
657
+ const publicInputs: `0x${string}`[] = [
658
+ `0x${intentHashField}`,
659
+ `0x${commitmentX}`,
660
+ `0x${commitmentY}`,
661
+ `0x${recipientStealthField}`,
662
+ `0x${params.minOutputAmount.toString(16).padStart(16, '0')}`,
663
+ `0x${solverId}`,
664
+ `0x${params.fulfillmentTime.toString(16).padStart(16, '0')}`,
665
+ `0x${params.expiry.toString(16).padStart(16, '0')}`,
666
+ ]
667
+
668
+ // Create ZKProof object
669
+ const proof: ZKProof = {
670
+ type: 'fulfillment',
671
+ proof: `0x${Buffer.from(proofData.proof).toString('hex')}`,
672
+ publicInputs,
673
+ }
674
+
675
+ return {
676
+ proof,
677
+ publicInputs,
678
+ }
679
+ } catch (error) {
680
+ const message = error instanceof Error ? error.message : String(error)
681
+
682
+ // Check for specific circuit errors
683
+ if (message.includes('Output below minimum')) {
684
+ throw new ProofGenerationError(
685
+ 'fulfillment',
686
+ 'Output amount is below minimum required',
687
+ error instanceof Error ? error : undefined
688
+ )
689
+ }
690
+ if (message.includes('Commitment') && message.includes('mismatch')) {
691
+ throw new ProofGenerationError(
692
+ 'fulfillment',
693
+ 'Output commitment verification failed',
694
+ error instanceof Error ? error : undefined
695
+ )
696
+ }
697
+ if (message.includes('Recipient mismatch')) {
698
+ throw new ProofGenerationError(
699
+ 'fulfillment',
700
+ 'Attestation recipient does not match',
701
+ error instanceof Error ? error : undefined
702
+ )
703
+ }
704
+ if (message.includes('Invalid oracle')) {
705
+ throw new ProofGenerationError(
706
+ 'fulfillment',
707
+ 'Oracle attestation signature is invalid',
708
+ error instanceof Error ? error : undefined
709
+ )
710
+ }
711
+ if (message.includes('Unauthorized solver')) {
712
+ throw new ProofGenerationError(
713
+ 'fulfillment',
714
+ 'Solver not authorized for this intent',
715
+ error instanceof Error ? error : undefined
716
+ )
717
+ }
718
+ if (message.includes('Fulfillment after expiry')) {
719
+ throw new ProofGenerationError(
720
+ 'fulfillment',
721
+ 'Fulfillment occurred after intent expiry',
722
+ error instanceof Error ? error : undefined
723
+ )
724
+ }
725
+
726
+ throw new ProofGenerationError(
727
+ 'fulfillment',
728
+ `Failed to generate fulfillment proof: ${message}`,
729
+ error instanceof Error ? error : undefined
730
+ )
731
+ }
196
732
  }
197
733
 
198
734
  /**
199
735
  * Verify a Noir proof
200
736
  */
201
- async verifyProof(_proof: ZKProof): Promise<boolean> {
737
+ async verifyProof(proof: ZKProof): Promise<boolean> {
202
738
  this.ensureReady()
203
739
 
204
- // TODO: Implement when circuits are ready
205
- //
206
- // Implementation will:
207
- // 1. Determine proof type from proof.type
208
- // 2. Use appropriate verifier circuit
209
- // 3. Return verification result
210
- //
211
- // Example:
212
- // ```typescript
213
- // const verified = await this.backend.verifyProof(proof)
214
- // return verified
215
- // ```
216
-
217
- throw new ProofError(
218
- 'Noir proof verification not yet implemented.',
219
- ErrorCode.PROOF_NOT_IMPLEMENTED
220
- )
740
+ // Select the appropriate backend based on proof type
741
+ let backend: UltraHonkBackend | null = null
742
+
743
+ switch (proof.type) {
744
+ case 'funding':
745
+ backend = this.fundingBackend
746
+ break
747
+ case 'validity':
748
+ backend = this.validityBackend
749
+ break
750
+ case 'fulfillment':
751
+ backend = this.fulfillmentBackend
752
+ break
753
+ default:
754
+ throw new ProofError(
755
+ `Unknown proof type: ${proof.type}`,
756
+ ErrorCode.PROOF_NOT_IMPLEMENTED
757
+ )
758
+ }
759
+
760
+ if (!backend) {
761
+ throw new ProofError(
762
+ `${proof.type} backend not initialized`,
763
+ ErrorCode.PROOF_PROVIDER_NOT_READY
764
+ )
765
+ }
766
+
767
+ try {
768
+ // Convert hex proof back to bytes
769
+ const proofHex = proof.proof.startsWith('0x') ? proof.proof.slice(2) : proof.proof
770
+ const proofBytes = new Uint8Array(Buffer.from(proofHex, 'hex'))
771
+
772
+ // Verify the proof
773
+ const isValid = await backend.verifyProof({
774
+ proof: proofBytes,
775
+ publicInputs: proof.publicInputs.map(input =>
776
+ input.startsWith('0x') ? input.slice(2) : input
777
+ ),
778
+ })
779
+
780
+ return isValid
781
+ } catch (error) {
782
+ if (this.config.verbose) {
783
+ console.error('[NoirProofProvider] Verification error:', error)
784
+ }
785
+ return false
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Destroy the provider and free resources
791
+ */
792
+ async destroy(): Promise<void> {
793
+ if (this.fundingBackend) {
794
+ await this.fundingBackend.destroy()
795
+ this.fundingBackend = null
796
+ }
797
+ if (this.validityBackend) {
798
+ await this.validityBackend.destroy()
799
+ this.validityBackend = null
800
+ }
801
+ if (this.fulfillmentBackend) {
802
+ await this.fulfillmentBackend.destroy()
803
+ this.fulfillmentBackend = null
804
+ }
805
+ this.fundingNoir = null
806
+ this.validityNoir = null
807
+ this.fulfillmentNoir = null
808
+ this._isReady = false
221
809
  }
222
810
 
223
811
  // ─── Private Methods ───────────────────────────────────────────────────────
@@ -230,4 +818,286 @@ export class NoirProofProvider implements ProofProvider {
230
818
  )
231
819
  }
232
820
  }
821
+
822
+ /**
823
+ * Compute the commitment hash that the circuit expects
824
+ *
825
+ * The circuit computes:
826
+ * 1. commitment = pedersen_commitment([balance, blinding])
827
+ * 2. commitment_hash = pedersen_hash([commitment.x, commitment.y, asset_id])
828
+ *
829
+ * We need to compute this outside to pass as a public input.
830
+ *
831
+ * **IMPORTANT**: This SDK uses SHA256 as a deterministic stand-in for Pedersen hash.
832
+ * Both the SDK and circuit MUST use the same hash function. The bundled circuit
833
+ * artifacts are configured to use SHA256 for compatibility. If you use custom
834
+ * circuits with actual Pedersen hashing, you must update this implementation.
835
+ *
836
+ * @see docs/specs/HASH-COMPATIBILITY.md for hash function requirements
837
+ */
838
+ private async computeCommitmentHash(
839
+ balance: bigint,
840
+ blindingFactor: Uint8Array,
841
+ assetId: string
842
+ ): Promise<{ commitmentHash: string; blindingField: string }> {
843
+ // Convert blinding factor to field element
844
+ const blindingField = this.bytesToField(blindingFactor)
845
+
846
+ // SHA256 is used for both SDK and circuit for hash compatibility
847
+ // The circuit artifacts bundled with this SDK are compiled to use SHA256
848
+ const { sha256 } = await import('@noble/hashes/sha256')
849
+ const { bytesToHex } = await import('@noble/hashes/utils')
850
+
851
+ // Create a deterministic commitment hash
852
+ // Preimage: balance (8 bytes) || blinding (32 bytes) || asset_id (32 bytes)
853
+ const preimage = new Uint8Array([
854
+ ...this.bigintToBytes(balance, 8),
855
+ ...blindingFactor.slice(0, 32),
856
+ ...this.hexToBytes(this.assetIdToField(assetId)),
857
+ ])
858
+
859
+ const hash = sha256(preimage)
860
+ const commitmentHash = bytesToHex(hash)
861
+
862
+ return { commitmentHash, blindingField }
863
+ }
864
+
865
+ /**
866
+ * Convert asset ID to field element
867
+ */
868
+ private assetIdToField(assetId: string): string {
869
+ // If it's already a hex string, use it directly
870
+ if (assetId.startsWith('0x')) {
871
+ return assetId.slice(2).padStart(64, '0')
872
+ }
873
+ // Otherwise, hash the string to get a field element
874
+ const encoder = new TextEncoder()
875
+ const bytes = encoder.encode(assetId)
876
+ let result = 0n
877
+ for (let i = 0; i < bytes.length && i < 31; i++) {
878
+ result = result * 256n + BigInt(bytes[i])
879
+ }
880
+ return result.toString(16).padStart(64, '0')
881
+ }
882
+
883
+ /**
884
+ * Convert bytes to field element string
885
+ */
886
+ private bytesToField(bytes: Uint8Array): string {
887
+ let result = 0n
888
+ const len = Math.min(bytes.length, 31) // Field element max 31 bytes
889
+ for (let i = 0; i < len; i++) {
890
+ result = result * 256n + BigInt(bytes[i])
891
+ }
892
+ return result.toString()
893
+ }
894
+
895
+ /**
896
+ * Convert bigint to bytes
897
+ */
898
+ private bigintToBytes(value: bigint, length: number): Uint8Array {
899
+ const bytes = new Uint8Array(length)
900
+ let v = value
901
+ for (let i = length - 1; i >= 0; i--) {
902
+ bytes[i] = Number(v & 0xffn)
903
+ v = v >> 8n
904
+ }
905
+ return bytes
906
+ }
907
+
908
+ /**
909
+ * Convert hex string to bytes
910
+ */
911
+ private hexToBytes(hex: string): Uint8Array {
912
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex
913
+ const bytes = new Uint8Array(h.length / 2)
914
+ for (let i = 0; i < bytes.length; i++) {
915
+ bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16)
916
+ }
917
+ return bytes
918
+ }
919
+
920
+ /**
921
+ * Convert hex string to field element string
922
+ */
923
+ private hexToField(hex: string): string {
924
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex
925
+ // Pad to 64 chars (32 bytes) for consistency
926
+ return h.padStart(64, '0')
927
+ }
928
+
929
+ /**
930
+ * Convert field string to 32-byte array
931
+ */
932
+ private fieldToBytes32(field: string): number[] {
933
+ const hex = field.padStart(64, '0')
934
+ const bytes: number[] = []
935
+ for (let i = 0; i < 32; i++) {
936
+ bytes.push(parseInt(hex.slice(i * 2, i * 2 + 2), 16))
937
+ }
938
+ return bytes
939
+ }
940
+
941
+ /**
942
+ * Compute sender commitment for validity proof
943
+ *
944
+ * Uses SHA256 for SDK-side computation. The bundled circuit artifacts
945
+ * are compiled to use SHA256 for compatibility with this SDK.
946
+ *
947
+ * @see computeCommitmentHash for hash function compatibility notes
948
+ */
949
+ private async computeSenderCommitment(
950
+ senderAddressField: string,
951
+ senderBlindingField: string
952
+ ): Promise<{ commitmentX: string; commitmentY: string }> {
953
+ const { sha256 } = await import('@noble/hashes/sha256')
954
+ const { bytesToHex } = await import('@noble/hashes/utils')
955
+
956
+ // Simulate commitment: hash(address || blinding)
957
+ const addressBytes = this.hexToBytes(senderAddressField)
958
+ const blindingBytes = this.hexToBytes(senderBlindingField.padStart(64, '0'))
959
+
960
+ const preimage = new Uint8Array([...addressBytes, ...blindingBytes])
961
+ const hash = sha256(preimage)
962
+
963
+ // Split hash into x and y components (16 bytes each)
964
+ const commitmentX = bytesToHex(hash.slice(0, 16)).padStart(64, '0')
965
+ const commitmentY = bytesToHex(hash.slice(16, 32)).padStart(64, '0')
966
+
967
+ return { commitmentX, commitmentY }
968
+ }
969
+
970
+ /**
971
+ * Compute nullifier for validity proof
972
+ *
973
+ * Uses SHA256 for SDK-side computation. The bundled circuit artifacts
974
+ * are compiled to use SHA256 for compatibility with this SDK.
975
+ *
976
+ * @see computeCommitmentHash for hash function compatibility notes
977
+ */
978
+ private async computeNullifier(
979
+ senderSecretField: string,
980
+ intentHashField: string,
981
+ nonceField: string
982
+ ): Promise<string> {
983
+ const { sha256 } = await import('@noble/hashes/sha256')
984
+ const { bytesToHex } = await import('@noble/hashes/utils')
985
+
986
+ // Simulate nullifier: hash(secret || intent_hash || nonce)
987
+ const secretBytes = this.hexToBytes(senderSecretField.padStart(64, '0'))
988
+ const intentBytes = this.hexToBytes(intentHashField)
989
+ const nonceBytes = this.hexToBytes(nonceField.padStart(64, '0'))
990
+
991
+ const preimage = new Uint8Array([...secretBytes, ...intentBytes, ...nonceBytes])
992
+ const hash = sha256(preimage)
993
+
994
+ return bytesToHex(hash)
995
+ }
996
+
997
+ /**
998
+ * Compute output commitment for fulfillment proof
999
+ *
1000
+ * Uses SHA256 for SDK-side computation. The bundled circuit artifacts
1001
+ * are compiled to use SHA256 for compatibility with this SDK.
1002
+ *
1003
+ * @see computeCommitmentHash for hash function compatibility notes
1004
+ */
1005
+ private async computeOutputCommitment(
1006
+ outputAmount: bigint,
1007
+ outputBlinding: Uint8Array
1008
+ ): Promise<{ commitmentX: string; commitmentY: string }> {
1009
+ const { sha256 } = await import('@noble/hashes/sha256')
1010
+ const { bytesToHex } = await import('@noble/hashes/utils')
1011
+
1012
+ // Simulate commitment: hash(amount || blinding)
1013
+ const amountBytes = this.bigintToBytes(outputAmount, 8)
1014
+ const blindingBytes = outputBlinding.slice(0, 32)
1015
+
1016
+ const preimage = new Uint8Array([...amountBytes, ...blindingBytes])
1017
+ const hash = sha256(preimage)
1018
+
1019
+ // Split hash into x and y components (16 bytes each)
1020
+ const commitmentX = bytesToHex(hash.slice(0, 16)).padStart(64, '0')
1021
+ const commitmentY = bytesToHex(hash.slice(16, 32)).padStart(64, '0')
1022
+
1023
+ return { commitmentX, commitmentY }
1024
+ }
1025
+
1026
+ /**
1027
+ * Compute solver ID from solver secret
1028
+ *
1029
+ * Uses SHA256 for SDK-side computation. The bundled circuit artifacts
1030
+ * are compiled to use SHA256 for compatibility with this SDK.
1031
+ *
1032
+ * @see computeCommitmentHash for hash function compatibility notes
1033
+ */
1034
+ private async computeSolverId(solverSecretField: string): Promise<string> {
1035
+ const { sha256 } = await import('@noble/hashes/sha256')
1036
+ const { bytesToHex } = await import('@noble/hashes/utils')
1037
+
1038
+ // Simulate solver_id: hash(solver_secret)
1039
+ const secretBytes = this.hexToBytes(solverSecretField.padStart(64, '0'))
1040
+ const hash = sha256(secretBytes)
1041
+
1042
+ return bytesToHex(hash)
1043
+ }
1044
+
1045
+ /**
1046
+ * Compute oracle message hash for fulfillment proof
1047
+ *
1048
+ * Hash of attestation data that oracle signs
1049
+ */
1050
+ private async computeOracleMessageHash(
1051
+ recipient: string,
1052
+ amount: bigint,
1053
+ txHash: string,
1054
+ blockNumber: bigint
1055
+ ): Promise<number[]> {
1056
+ const { sha256 } = await import('@noble/hashes/sha256')
1057
+
1058
+ // Hash: recipient || amount || txHash || blockNumber
1059
+ const recipientBytes = this.hexToBytes(this.hexToField(recipient))
1060
+ const amountBytes = this.bigintToBytes(amount, 8)
1061
+ const txHashBytes = this.hexToBytes(this.hexToField(txHash))
1062
+ const blockBytes = this.bigintToBytes(blockNumber, 8)
1063
+
1064
+ const preimage = new Uint8Array([
1065
+ ...recipientBytes,
1066
+ ...amountBytes,
1067
+ ...txHashBytes,
1068
+ ...blockBytes,
1069
+ ])
1070
+ const hash = sha256(preimage)
1071
+
1072
+ return Array.from(hash)
1073
+ }
1074
+
1075
+ /**
1076
+ * Derive secp256k1 public key coordinates from a private key
1077
+ *
1078
+ * @param privateKey - 32-byte private key as Uint8Array
1079
+ * @returns X and Y coordinates as 32-byte arrays
1080
+ */
1081
+ private getPublicKeyCoordinates(privateKey: Uint8Array): PublicKeyCoordinates {
1082
+ // Get uncompressed public key (65 bytes: 04 || x || y)
1083
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
1084
+
1085
+ // Extract X (bytes 1-32) and Y (bytes 33-64)
1086
+ const x = Array.from(uncompressedPubKey.slice(1, 33))
1087
+ const y = Array.from(uncompressedPubKey.slice(33, 65))
1088
+
1089
+ return { x, y }
1090
+ }
1091
+
1092
+ /**
1093
+ * Derive public key coordinates from a field string (private key)
1094
+ *
1095
+ * @param privateKeyField - Private key as hex field string
1096
+ * @returns X and Y coordinates as 32-byte arrays
1097
+ */
1098
+ private getPublicKeyFromField(privateKeyField: string): PublicKeyCoordinates {
1099
+ // Convert field to 32-byte array
1100
+ const privateKeyBytes = this.hexToBytes(privateKeyField.padStart(64, '0'))
1101
+ return this.getPublicKeyCoordinates(privateKeyBytes)
1102
+ }
233
1103
  }