@sip-protocol/sdk 0.1.9 → 0.2.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.
- package/LICENSE +21 -0
- package/dist/browser.d.mts +2 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +12925 -0
- package/dist/browser.mjs +1037 -0
- package/dist/chunk-4VJHI66K.mjs +12120 -0
- package/dist/chunk-O4Y2ZUDL.mjs +12721 -0
- package/dist/index.d.mts +800 -91
- package/dist/index.d.ts +800 -91
- package/dist/index.js +1137 -266
- package/dist/index.mjs +260 -11114
- package/package.json +20 -14
- package/src/adapters/near-intents.ts +138 -30
- package/src/browser.ts +35 -0
- package/src/commitment.ts +4 -4
- package/src/index.ts +72 -0
- package/src/oracle/index.ts +12 -0
- package/src/oracle/serialization.ts +237 -0
- package/src/oracle/types.ts +257 -0
- package/src/oracle/verification.ts +257 -0
- package/src/proofs/browser-utils.ts +141 -0
- package/src/proofs/browser.ts +884 -0
- package/src/proofs/index.ts +19 -1
- package/src/stealth.ts +868 -12
- package/src/validation.ts +7 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Noir Proof Provider
|
|
3
|
+
*
|
|
4
|
+
* Production-ready ZK proof provider for browser environments.
|
|
5
|
+
* Uses Web Workers for non-blocking proof generation and WASM for computation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { BrowserNoirProvider } from '@sip-protocol/sdk/browser'
|
|
10
|
+
*
|
|
11
|
+
* const provider = new BrowserNoirProvider()
|
|
12
|
+
* await provider.initialize() // Loads WASM
|
|
13
|
+
*
|
|
14
|
+
* const proof = await provider.generateFundingProof(inputs)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @see docs/specs/ZK-ARCHITECTURE.md
|
|
18
|
+
* @see https://github.com/sip-protocol/sip-protocol/issues/121
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ZKProof } from '@sip-protocol/types'
|
|
22
|
+
import type {
|
|
23
|
+
ProofProvider,
|
|
24
|
+
ProofFramework,
|
|
25
|
+
FundingProofParams,
|
|
26
|
+
ValidityProofParams,
|
|
27
|
+
FulfillmentProofParams,
|
|
28
|
+
ProofResult,
|
|
29
|
+
} from './interface'
|
|
30
|
+
import { ProofGenerationError } from './interface'
|
|
31
|
+
import { ProofError, ErrorCode } from '../errors'
|
|
32
|
+
import {
|
|
33
|
+
isBrowser,
|
|
34
|
+
supportsWebWorkers,
|
|
35
|
+
supportsSharedArrayBuffer,
|
|
36
|
+
hexToBytes,
|
|
37
|
+
bytesToHex,
|
|
38
|
+
getBrowserInfo,
|
|
39
|
+
} from './browser-utils'
|
|
40
|
+
|
|
41
|
+
// Import Noir JS (works in browser with WASM)
|
|
42
|
+
import { Noir } from '@noir-lang/noir_js'
|
|
43
|
+
import type { CompiledCircuit } from '@noir-lang/types'
|
|
44
|
+
import { UltraHonkBackend } from '@aztec/bb.js'
|
|
45
|
+
import { secp256k1 } from '@noble/curves/secp256k1'
|
|
46
|
+
|
|
47
|
+
// Import compiled circuit artifacts
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
import fundingCircuitArtifact from './circuits/funding_proof.json'
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
import validityCircuitArtifact from './circuits/validity_proof.json'
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
import fulfillmentCircuitArtifact from './circuits/fulfillment_proof.json'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Public key coordinates for secp256k1
|
|
57
|
+
*/
|
|
58
|
+
export interface PublicKeyCoordinates {
|
|
59
|
+
/** X coordinate as 32-byte array */
|
|
60
|
+
x: number[]
|
|
61
|
+
/** Y coordinate as 32-byte array */
|
|
62
|
+
y: number[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Browser Noir Provider Configuration
|
|
67
|
+
*/
|
|
68
|
+
export interface BrowserNoirProviderConfig {
|
|
69
|
+
/**
|
|
70
|
+
* Use Web Workers for proof generation (non-blocking)
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
useWorker?: boolean
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enable verbose logging for debugging
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
verbose?: boolean
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Oracle public key for verifying attestations in fulfillment proofs
|
|
83
|
+
* Required for production use.
|
|
84
|
+
*/
|
|
85
|
+
oraclePublicKey?: PublicKeyCoordinates
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Maximum time for proof generation before timeout (ms)
|
|
89
|
+
* @default 60000 (60 seconds)
|
|
90
|
+
*/
|
|
91
|
+
timeout?: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Proof generation progress callback
|
|
96
|
+
*/
|
|
97
|
+
export type ProofProgressCallback = (progress: {
|
|
98
|
+
stage: 'initializing' | 'witness' | 'proving' | 'verifying' | 'complete'
|
|
99
|
+
percent: number
|
|
100
|
+
message: string
|
|
101
|
+
}) => void
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Browser-compatible Noir Proof Provider
|
|
105
|
+
*
|
|
106
|
+
* Designed for browser environments with:
|
|
107
|
+
* - WASM-based proof generation
|
|
108
|
+
* - Optional Web Worker support for non-blocking UI
|
|
109
|
+
* - Memory-efficient initialization
|
|
110
|
+
* - Progress callbacks for UX
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const provider = new BrowserNoirProvider({ useWorker: true })
|
|
115
|
+
*
|
|
116
|
+
* await provider.initialize((progress) => {
|
|
117
|
+
* console.log(`${progress.stage}: ${progress.percent}%`)
|
|
118
|
+
* })
|
|
119
|
+
*
|
|
120
|
+
* const result = await provider.generateFundingProof(params, (progress) => {
|
|
121
|
+
* updateProgressBar(progress.percent)
|
|
122
|
+
* })
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export class BrowserNoirProvider implements ProofProvider {
|
|
126
|
+
readonly framework: ProofFramework = 'noir'
|
|
127
|
+
private _isReady = false
|
|
128
|
+
private config: Required<BrowserNoirProviderConfig>
|
|
129
|
+
|
|
130
|
+
// Circuit instances
|
|
131
|
+
private fundingNoir: Noir | null = null
|
|
132
|
+
private fundingBackend: UltraHonkBackend | null = null
|
|
133
|
+
private validityNoir: Noir | null = null
|
|
134
|
+
private validityBackend: UltraHonkBackend | null = null
|
|
135
|
+
private fulfillmentNoir: Noir | null = null
|
|
136
|
+
private fulfillmentBackend: UltraHonkBackend | null = null
|
|
137
|
+
|
|
138
|
+
// Worker instance (optional)
|
|
139
|
+
private worker: Worker | null = null
|
|
140
|
+
private workerPending: Map<
|
|
141
|
+
string,
|
|
142
|
+
{ resolve: (result: ProofResult) => void; reject: (error: Error) => void }
|
|
143
|
+
> = new Map()
|
|
144
|
+
|
|
145
|
+
constructor(config: BrowserNoirProviderConfig = {}) {
|
|
146
|
+
this.config = {
|
|
147
|
+
useWorker: config.useWorker ?? true,
|
|
148
|
+
verbose: config.verbose ?? false,
|
|
149
|
+
oraclePublicKey: config.oraclePublicKey ?? undefined,
|
|
150
|
+
timeout: config.timeout ?? 60000,
|
|
151
|
+
} as Required<BrowserNoirProviderConfig>
|
|
152
|
+
|
|
153
|
+
// Warn if not in browser
|
|
154
|
+
if (!isBrowser()) {
|
|
155
|
+
console.warn(
|
|
156
|
+
'[BrowserNoirProvider] Not running in browser environment. ' +
|
|
157
|
+
'Consider using NoirProofProvider for Node.js.'
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get isReady(): boolean {
|
|
163
|
+
return this._isReady
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get browser environment info
|
|
168
|
+
*/
|
|
169
|
+
static getBrowserInfo() {
|
|
170
|
+
return getBrowserInfo()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if browser supports all required features
|
|
175
|
+
*/
|
|
176
|
+
static checkBrowserSupport(): {
|
|
177
|
+
supported: boolean
|
|
178
|
+
missing: string[]
|
|
179
|
+
} {
|
|
180
|
+
const missing: string[] = []
|
|
181
|
+
|
|
182
|
+
if (!isBrowser()) {
|
|
183
|
+
missing.push('browser environment')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for WebAssembly
|
|
187
|
+
if (typeof WebAssembly === 'undefined') {
|
|
188
|
+
missing.push('WebAssembly')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// SharedArrayBuffer is required for Barretenberg WASM
|
|
192
|
+
if (!supportsSharedArrayBuffer()) {
|
|
193
|
+
missing.push('SharedArrayBuffer (requires COOP/COEP headers)')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
supported: missing.length === 0,
|
|
198
|
+
missing,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Derive secp256k1 public key coordinates from a private key
|
|
204
|
+
*/
|
|
205
|
+
static derivePublicKey(privateKey: Uint8Array): PublicKeyCoordinates {
|
|
206
|
+
const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
|
|
207
|
+
const x = Array.from(uncompressedPubKey.slice(1, 33))
|
|
208
|
+
const y = Array.from(uncompressedPubKey.slice(33, 65))
|
|
209
|
+
return { x, y }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Initialize the browser provider
|
|
214
|
+
*
|
|
215
|
+
* Loads WASM and circuit artifacts. This should be called before any
|
|
216
|
+
* proof generation. Consider showing a loading indicator during init.
|
|
217
|
+
*
|
|
218
|
+
* @param onProgress - Optional progress callback
|
|
219
|
+
*/
|
|
220
|
+
async initialize(onProgress?: ProofProgressCallback): Promise<void> {
|
|
221
|
+
if (this._isReady) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { supported, missing } = BrowserNoirProvider.checkBrowserSupport()
|
|
226
|
+
if (!supported) {
|
|
227
|
+
throw new ProofError(
|
|
228
|
+
`Browser missing required features: ${missing.join(', ')}`,
|
|
229
|
+
ErrorCode.PROOF_PROVIDER_NOT_READY
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
onProgress?.({
|
|
235
|
+
stage: 'initializing',
|
|
236
|
+
percent: 0,
|
|
237
|
+
message: 'Loading WASM runtime...',
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
if (this.config.verbose) {
|
|
241
|
+
console.log('[BrowserNoirProvider] Initializing...')
|
|
242
|
+
console.log('[BrowserNoirProvider] Browser info:', getBrowserInfo())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Initialize circuits in parallel for faster loading
|
|
246
|
+
const fundingCircuit = fundingCircuitArtifact as unknown as CompiledCircuit
|
|
247
|
+
const validityCircuit = validityCircuitArtifact as unknown as CompiledCircuit
|
|
248
|
+
const fulfillmentCircuit = fulfillmentCircuitArtifact as unknown as CompiledCircuit
|
|
249
|
+
|
|
250
|
+
onProgress?.({
|
|
251
|
+
stage: 'initializing',
|
|
252
|
+
percent: 20,
|
|
253
|
+
message: 'Creating proof backends...',
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Create backends (this loads WASM)
|
|
257
|
+
this.fundingBackend = new UltraHonkBackend(fundingCircuit.bytecode)
|
|
258
|
+
this.validityBackend = new UltraHonkBackend(validityCircuit.bytecode)
|
|
259
|
+
this.fulfillmentBackend = new UltraHonkBackend(fulfillmentCircuit.bytecode)
|
|
260
|
+
|
|
261
|
+
onProgress?.({
|
|
262
|
+
stage: 'initializing',
|
|
263
|
+
percent: 60,
|
|
264
|
+
message: 'Initializing Noir circuits...',
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Create Noir instances for witness generation
|
|
268
|
+
this.fundingNoir = new Noir(fundingCircuit)
|
|
269
|
+
this.validityNoir = new Noir(validityCircuit)
|
|
270
|
+
this.fulfillmentNoir = new Noir(fulfillmentCircuit)
|
|
271
|
+
|
|
272
|
+
onProgress?.({
|
|
273
|
+
stage: 'initializing',
|
|
274
|
+
percent: 90,
|
|
275
|
+
message: 'Setting up worker...',
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Initialize worker if enabled and supported
|
|
279
|
+
if (this.config.useWorker && supportsWebWorkers()) {
|
|
280
|
+
await this.initializeWorker()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._isReady = true
|
|
284
|
+
|
|
285
|
+
onProgress?.({
|
|
286
|
+
stage: 'complete',
|
|
287
|
+
percent: 100,
|
|
288
|
+
message: 'Ready for proof generation',
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
if (this.config.verbose) {
|
|
292
|
+
console.log('[BrowserNoirProvider] Initialization complete')
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new ProofError(
|
|
296
|
+
`Failed to initialize BrowserNoirProvider: ${error instanceof Error ? error.message : String(error)}`,
|
|
297
|
+
ErrorCode.PROOF_NOT_IMPLEMENTED,
|
|
298
|
+
{ context: { error } }
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Initialize Web Worker for off-main-thread proof generation
|
|
305
|
+
*/
|
|
306
|
+
private async initializeWorker(): Promise<void> {
|
|
307
|
+
// Worker initialization is optional - proof gen works on main thread too
|
|
308
|
+
// For now, we'll do main-thread proof gen with async/await
|
|
309
|
+
// Full worker implementation would require bundling worker code separately
|
|
310
|
+
if (this.config.verbose) {
|
|
311
|
+
console.log('[BrowserNoirProvider] Worker support: using async main-thread')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Generate a Funding Proof
|
|
317
|
+
*
|
|
318
|
+
* Proves: balance >= minimumRequired without revealing balance
|
|
319
|
+
*
|
|
320
|
+
* @param params - Funding proof parameters
|
|
321
|
+
* @param onProgress - Optional progress callback
|
|
322
|
+
*/
|
|
323
|
+
async generateFundingProof(
|
|
324
|
+
params: FundingProofParams,
|
|
325
|
+
onProgress?: ProofProgressCallback
|
|
326
|
+
): Promise<ProofResult> {
|
|
327
|
+
this.ensureReady()
|
|
328
|
+
|
|
329
|
+
if (!this.fundingNoir || !this.fundingBackend) {
|
|
330
|
+
throw new ProofGenerationError('funding', 'Funding circuit not initialized')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
onProgress?.({
|
|
335
|
+
stage: 'witness',
|
|
336
|
+
percent: 10,
|
|
337
|
+
message: 'Preparing witness inputs...',
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Compute commitment hash
|
|
341
|
+
const { commitmentHash, blindingField } = await this.computeCommitmentHash(
|
|
342
|
+
params.balance,
|
|
343
|
+
params.blindingFactor,
|
|
344
|
+
params.assetId
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const witnessInputs = {
|
|
348
|
+
commitment_hash: commitmentHash,
|
|
349
|
+
minimum_required: params.minimumRequired.toString(),
|
|
350
|
+
asset_id: this.assetIdToField(params.assetId),
|
|
351
|
+
balance: params.balance.toString(),
|
|
352
|
+
blinding: blindingField,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
onProgress?.({
|
|
356
|
+
stage: 'witness',
|
|
357
|
+
percent: 30,
|
|
358
|
+
message: 'Generating witness...',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Generate witness
|
|
362
|
+
const { witness } = await this.fundingNoir.execute(witnessInputs)
|
|
363
|
+
|
|
364
|
+
onProgress?.({
|
|
365
|
+
stage: 'proving',
|
|
366
|
+
percent: 50,
|
|
367
|
+
message: 'Generating proof (this may take a moment)...',
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Generate proof
|
|
371
|
+
const proofData = await this.fundingBackend.generateProof(witness)
|
|
372
|
+
|
|
373
|
+
onProgress?.({
|
|
374
|
+
stage: 'complete',
|
|
375
|
+
percent: 100,
|
|
376
|
+
message: 'Proof generated successfully',
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const publicInputs: `0x${string}`[] = [
|
|
380
|
+
`0x${commitmentHash}`,
|
|
381
|
+
`0x${params.minimumRequired.toString(16).padStart(16, '0')}`,
|
|
382
|
+
`0x${this.assetIdToField(params.assetId)}`,
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
const proof: ZKProof = {
|
|
386
|
+
type: 'funding',
|
|
387
|
+
proof: `0x${bytesToHex(proofData.proof)}`,
|
|
388
|
+
publicInputs,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { proof, publicInputs }
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
394
|
+
throw new ProofGenerationError(
|
|
395
|
+
'funding',
|
|
396
|
+
`Failed to generate funding proof: ${message}`,
|
|
397
|
+
error instanceof Error ? error : undefined
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate a Validity Proof
|
|
404
|
+
*
|
|
405
|
+
* Proves: Intent is authorized by sender without revealing identity
|
|
406
|
+
*/
|
|
407
|
+
async generateValidityProof(
|
|
408
|
+
params: ValidityProofParams,
|
|
409
|
+
onProgress?: ProofProgressCallback
|
|
410
|
+
): Promise<ProofResult> {
|
|
411
|
+
this.ensureReady()
|
|
412
|
+
|
|
413
|
+
if (!this.validityNoir || !this.validityBackend) {
|
|
414
|
+
throw new ProofGenerationError('validity', 'Validity circuit not initialized')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
onProgress?.({
|
|
419
|
+
stage: 'witness',
|
|
420
|
+
percent: 10,
|
|
421
|
+
message: 'Preparing validity witness...',
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// Convert inputs to field elements
|
|
425
|
+
const intentHashField = this.hexToField(params.intentHash)
|
|
426
|
+
const senderAddressField = this.hexToField(params.senderAddress)
|
|
427
|
+
const senderBlindingField = this.bytesToField(params.senderBlinding)
|
|
428
|
+
const senderSecretField = this.bytesToField(params.senderSecret)
|
|
429
|
+
const nonceField = this.bytesToField(params.nonce)
|
|
430
|
+
|
|
431
|
+
// Compute derived values
|
|
432
|
+
const { commitmentX, commitmentY } = await this.computeSenderCommitment(
|
|
433
|
+
senderAddressField,
|
|
434
|
+
senderBlindingField
|
|
435
|
+
)
|
|
436
|
+
const nullifier = await this.computeNullifier(senderSecretField, intentHashField, nonceField)
|
|
437
|
+
|
|
438
|
+
const signature = Array.from(params.authorizationSignature)
|
|
439
|
+
const messageHash = this.fieldToBytes32(intentHashField)
|
|
440
|
+
|
|
441
|
+
// Get public key
|
|
442
|
+
let pubKeyX: number[]
|
|
443
|
+
let pubKeyY: number[]
|
|
444
|
+
if (params.senderPublicKey) {
|
|
445
|
+
pubKeyX = Array.from(params.senderPublicKey.x)
|
|
446
|
+
pubKeyY = Array.from(params.senderPublicKey.y)
|
|
447
|
+
} else {
|
|
448
|
+
const coords = this.getPublicKeyCoordinates(params.senderSecret)
|
|
449
|
+
pubKeyX = coords.x
|
|
450
|
+
pubKeyY = coords.y
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const witnessInputs = {
|
|
454
|
+
intent_hash: intentHashField,
|
|
455
|
+
sender_commitment_x: commitmentX,
|
|
456
|
+
sender_commitment_y: commitmentY,
|
|
457
|
+
nullifier: nullifier,
|
|
458
|
+
timestamp: params.timestamp.toString(),
|
|
459
|
+
expiry: params.expiry.toString(),
|
|
460
|
+
sender_address: senderAddressField,
|
|
461
|
+
sender_blinding: senderBlindingField,
|
|
462
|
+
sender_secret: senderSecretField,
|
|
463
|
+
pub_key_x: pubKeyX,
|
|
464
|
+
pub_key_y: pubKeyY,
|
|
465
|
+
signature: signature,
|
|
466
|
+
message_hash: messageHash,
|
|
467
|
+
nonce: nonceField,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
onProgress?.({
|
|
471
|
+
stage: 'witness',
|
|
472
|
+
percent: 30,
|
|
473
|
+
message: 'Generating witness...',
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const { witness } = await this.validityNoir.execute(witnessInputs)
|
|
477
|
+
|
|
478
|
+
onProgress?.({
|
|
479
|
+
stage: 'proving',
|
|
480
|
+
percent: 50,
|
|
481
|
+
message: 'Generating validity proof...',
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const proofData = await this.validityBackend.generateProof(witness)
|
|
485
|
+
|
|
486
|
+
onProgress?.({
|
|
487
|
+
stage: 'complete',
|
|
488
|
+
percent: 100,
|
|
489
|
+
message: 'Validity proof generated',
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
const publicInputs: `0x${string}`[] = [
|
|
493
|
+
`0x${intentHashField}`,
|
|
494
|
+
`0x${commitmentX}`,
|
|
495
|
+
`0x${commitmentY}`,
|
|
496
|
+
`0x${nullifier}`,
|
|
497
|
+
`0x${params.timestamp.toString(16).padStart(16, '0')}`,
|
|
498
|
+
`0x${params.expiry.toString(16).padStart(16, '0')}`,
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
const proof: ZKProof = {
|
|
502
|
+
type: 'validity',
|
|
503
|
+
proof: `0x${bytesToHex(proofData.proof)}`,
|
|
504
|
+
publicInputs,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return { proof, publicInputs }
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
510
|
+
throw new ProofGenerationError(
|
|
511
|
+
'validity',
|
|
512
|
+
`Failed to generate validity proof: ${message}`,
|
|
513
|
+
error instanceof Error ? error : undefined
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Generate a Fulfillment Proof
|
|
520
|
+
*
|
|
521
|
+
* Proves: Solver correctly executed the intent
|
|
522
|
+
*/
|
|
523
|
+
async generateFulfillmentProof(
|
|
524
|
+
params: FulfillmentProofParams,
|
|
525
|
+
onProgress?: ProofProgressCallback
|
|
526
|
+
): Promise<ProofResult> {
|
|
527
|
+
this.ensureReady()
|
|
528
|
+
|
|
529
|
+
if (!this.fulfillmentNoir || !this.fulfillmentBackend) {
|
|
530
|
+
throw new ProofGenerationError('fulfillment', 'Fulfillment circuit not initialized')
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
onProgress?.({
|
|
535
|
+
stage: 'witness',
|
|
536
|
+
percent: 10,
|
|
537
|
+
message: 'Preparing fulfillment witness...',
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const intentHashField = this.hexToField(params.intentHash)
|
|
541
|
+
const recipientStealthField = this.hexToField(params.recipientStealth)
|
|
542
|
+
|
|
543
|
+
const { commitmentX, commitmentY } = await this.computeOutputCommitment(
|
|
544
|
+
params.outputAmount,
|
|
545
|
+
params.outputBlinding
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
const solverSecretField = this.bytesToField(params.solverSecret)
|
|
549
|
+
const solverId = await this.computeSolverId(solverSecretField)
|
|
550
|
+
const outputBlindingField = this.bytesToField(params.outputBlinding)
|
|
551
|
+
|
|
552
|
+
const attestation = params.oracleAttestation
|
|
553
|
+
const attestationRecipientField = this.hexToField(attestation.recipient)
|
|
554
|
+
const attestationTxHashField = this.hexToField(attestation.txHash)
|
|
555
|
+
const oracleSignature = Array.from(attestation.signature)
|
|
556
|
+
const oracleMessageHash = await this.computeOracleMessageHash(
|
|
557
|
+
attestation.recipient,
|
|
558
|
+
attestation.amount,
|
|
559
|
+
attestation.txHash,
|
|
560
|
+
attestation.blockNumber
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
const oraclePubKeyX = this.config.oraclePublicKey?.x ?? new Array(32).fill(0)
|
|
564
|
+
const oraclePubKeyY = this.config.oraclePublicKey?.y ?? new Array(32).fill(0)
|
|
565
|
+
|
|
566
|
+
const witnessInputs = {
|
|
567
|
+
intent_hash: intentHashField,
|
|
568
|
+
output_commitment_x: commitmentX,
|
|
569
|
+
output_commitment_y: commitmentY,
|
|
570
|
+
recipient_stealth: recipientStealthField,
|
|
571
|
+
min_output_amount: params.minOutputAmount.toString(),
|
|
572
|
+
solver_id: solverId,
|
|
573
|
+
fulfillment_time: params.fulfillmentTime.toString(),
|
|
574
|
+
expiry: params.expiry.toString(),
|
|
575
|
+
output_amount: params.outputAmount.toString(),
|
|
576
|
+
output_blinding: outputBlindingField,
|
|
577
|
+
solver_secret: solverSecretField,
|
|
578
|
+
attestation_recipient: attestationRecipientField,
|
|
579
|
+
attestation_amount: attestation.amount.toString(),
|
|
580
|
+
attestation_tx_hash: attestationTxHashField,
|
|
581
|
+
attestation_block: attestation.blockNumber.toString(),
|
|
582
|
+
oracle_signature: oracleSignature,
|
|
583
|
+
oracle_message_hash: oracleMessageHash,
|
|
584
|
+
oracle_pub_key_x: oraclePubKeyX,
|
|
585
|
+
oracle_pub_key_y: oraclePubKeyY,
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
onProgress?.({
|
|
589
|
+
stage: 'witness',
|
|
590
|
+
percent: 30,
|
|
591
|
+
message: 'Generating witness...',
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const { witness } = await this.fulfillmentNoir.execute(witnessInputs)
|
|
595
|
+
|
|
596
|
+
onProgress?.({
|
|
597
|
+
stage: 'proving',
|
|
598
|
+
percent: 50,
|
|
599
|
+
message: 'Generating fulfillment proof...',
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
const proofData = await this.fulfillmentBackend.generateProof(witness)
|
|
603
|
+
|
|
604
|
+
onProgress?.({
|
|
605
|
+
stage: 'complete',
|
|
606
|
+
percent: 100,
|
|
607
|
+
message: 'Fulfillment proof generated',
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
const publicInputs: `0x${string}`[] = [
|
|
611
|
+
`0x${intentHashField}`,
|
|
612
|
+
`0x${commitmentX}`,
|
|
613
|
+
`0x${commitmentY}`,
|
|
614
|
+
`0x${recipientStealthField}`,
|
|
615
|
+
`0x${params.minOutputAmount.toString(16).padStart(16, '0')}`,
|
|
616
|
+
`0x${solverId}`,
|
|
617
|
+
`0x${params.fulfillmentTime.toString(16).padStart(16, '0')}`,
|
|
618
|
+
`0x${params.expiry.toString(16).padStart(16, '0')}`,
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
const proof: ZKProof = {
|
|
622
|
+
type: 'fulfillment',
|
|
623
|
+
proof: `0x${bytesToHex(proofData.proof)}`,
|
|
624
|
+
publicInputs,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { proof, publicInputs }
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
630
|
+
throw new ProofGenerationError(
|
|
631
|
+
'fulfillment',
|
|
632
|
+
`Failed to generate fulfillment proof: ${message}`,
|
|
633
|
+
error instanceof Error ? error : undefined
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Verify a proof
|
|
640
|
+
*/
|
|
641
|
+
async verifyProof(proof: ZKProof): Promise<boolean> {
|
|
642
|
+
this.ensureReady()
|
|
643
|
+
|
|
644
|
+
let backend: UltraHonkBackend | null = null
|
|
645
|
+
|
|
646
|
+
switch (proof.type) {
|
|
647
|
+
case 'funding':
|
|
648
|
+
backend = this.fundingBackend
|
|
649
|
+
break
|
|
650
|
+
case 'validity':
|
|
651
|
+
backend = this.validityBackend
|
|
652
|
+
break
|
|
653
|
+
case 'fulfillment':
|
|
654
|
+
backend = this.fulfillmentBackend
|
|
655
|
+
break
|
|
656
|
+
default:
|
|
657
|
+
throw new ProofError(`Unknown proof type: ${proof.type}`, ErrorCode.PROOF_NOT_IMPLEMENTED)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!backend) {
|
|
661
|
+
throw new ProofError(
|
|
662
|
+
`${proof.type} backend not initialized`,
|
|
663
|
+
ErrorCode.PROOF_PROVIDER_NOT_READY
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const proofHex = proof.proof.startsWith('0x') ? proof.proof.slice(2) : proof.proof
|
|
669
|
+
const proofBytes = hexToBytes(proofHex)
|
|
670
|
+
|
|
671
|
+
const isValid = await backend.verifyProof({
|
|
672
|
+
proof: proofBytes,
|
|
673
|
+
publicInputs: proof.publicInputs.map((input) =>
|
|
674
|
+
input.startsWith('0x') ? input.slice(2) : input
|
|
675
|
+
),
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
return isValid
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (this.config.verbose) {
|
|
681
|
+
console.error('[BrowserNoirProvider] Verification error:', error)
|
|
682
|
+
}
|
|
683
|
+
return false
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Destroy the provider and free resources
|
|
689
|
+
*/
|
|
690
|
+
async destroy(): Promise<void> {
|
|
691
|
+
if (this.fundingBackend) {
|
|
692
|
+
await this.fundingBackend.destroy()
|
|
693
|
+
this.fundingBackend = null
|
|
694
|
+
}
|
|
695
|
+
if (this.validityBackend) {
|
|
696
|
+
await this.validityBackend.destroy()
|
|
697
|
+
this.validityBackend = null
|
|
698
|
+
}
|
|
699
|
+
if (this.fulfillmentBackend) {
|
|
700
|
+
await this.fulfillmentBackend.destroy()
|
|
701
|
+
this.fulfillmentBackend = null
|
|
702
|
+
}
|
|
703
|
+
if (this.worker) {
|
|
704
|
+
this.worker.terminate()
|
|
705
|
+
this.worker = null
|
|
706
|
+
}
|
|
707
|
+
this.fundingNoir = null
|
|
708
|
+
this.validityNoir = null
|
|
709
|
+
this.fulfillmentNoir = null
|
|
710
|
+
this._isReady = false
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ─── Private Utility Methods ────────────────────────────────────────────────
|
|
714
|
+
|
|
715
|
+
private ensureReady(): void {
|
|
716
|
+
if (!this._isReady) {
|
|
717
|
+
throw new ProofError(
|
|
718
|
+
'BrowserNoirProvider not initialized. Call initialize() first.',
|
|
719
|
+
ErrorCode.PROOF_PROVIDER_NOT_READY
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private async computeCommitmentHash(
|
|
725
|
+
balance: bigint,
|
|
726
|
+
blindingFactor: Uint8Array,
|
|
727
|
+
assetId: string
|
|
728
|
+
): Promise<{ commitmentHash: string; blindingField: string }> {
|
|
729
|
+
const blindingField = this.bytesToField(blindingFactor)
|
|
730
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
731
|
+
const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
|
|
732
|
+
|
|
733
|
+
const preimage = new Uint8Array([
|
|
734
|
+
...this.bigintToBytes(balance, 8),
|
|
735
|
+
...blindingFactor.slice(0, 32),
|
|
736
|
+
...hexToBytes(this.assetIdToField(assetId)),
|
|
737
|
+
])
|
|
738
|
+
|
|
739
|
+
const hash = sha256(preimage)
|
|
740
|
+
const commitmentHash = nobleToHex(hash)
|
|
741
|
+
|
|
742
|
+
return { commitmentHash, blindingField }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private assetIdToField(assetId: string): string {
|
|
746
|
+
if (assetId.startsWith('0x')) {
|
|
747
|
+
return assetId.slice(2).padStart(64, '0')
|
|
748
|
+
}
|
|
749
|
+
const encoder = new TextEncoder()
|
|
750
|
+
const bytes = encoder.encode(assetId)
|
|
751
|
+
let result = 0n
|
|
752
|
+
for (let i = 0; i < bytes.length && i < 31; i++) {
|
|
753
|
+
result = result * 256n + BigInt(bytes[i])
|
|
754
|
+
}
|
|
755
|
+
return result.toString(16).padStart(64, '0')
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private bytesToField(bytes: Uint8Array): string {
|
|
759
|
+
let result = 0n
|
|
760
|
+
const len = Math.min(bytes.length, 31)
|
|
761
|
+
for (let i = 0; i < len; i++) {
|
|
762
|
+
result = result * 256n + BigInt(bytes[i])
|
|
763
|
+
}
|
|
764
|
+
return result.toString()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private bigintToBytes(value: bigint, length: number): Uint8Array {
|
|
768
|
+
const bytes = new Uint8Array(length)
|
|
769
|
+
let v = value
|
|
770
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
771
|
+
bytes[i] = Number(v & 0xffn)
|
|
772
|
+
v = v >> 8n
|
|
773
|
+
}
|
|
774
|
+
return bytes
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private hexToField(hex: string): string {
|
|
778
|
+
const h = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
779
|
+
return h.padStart(64, '0')
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private fieldToBytes32(field: string): number[] {
|
|
783
|
+
const hex = field.padStart(64, '0')
|
|
784
|
+
const bytes: number[] = []
|
|
785
|
+
for (let i = 0; i < 32; i++) {
|
|
786
|
+
bytes.push(parseInt(hex.slice(i * 2, i * 2 + 2), 16))
|
|
787
|
+
}
|
|
788
|
+
return bytes
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private async computeSenderCommitment(
|
|
792
|
+
senderAddressField: string,
|
|
793
|
+
senderBlindingField: string
|
|
794
|
+
): Promise<{ commitmentX: string; commitmentY: string }> {
|
|
795
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
796
|
+
const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
|
|
797
|
+
|
|
798
|
+
const addressBytes = hexToBytes(senderAddressField)
|
|
799
|
+
const blindingBytes = hexToBytes(senderBlindingField.padStart(64, '0'))
|
|
800
|
+
const preimage = new Uint8Array([...addressBytes, ...blindingBytes])
|
|
801
|
+
const hash = sha256(preimage)
|
|
802
|
+
|
|
803
|
+
const commitmentX = nobleToHex(hash.slice(0, 16)).padStart(64, '0')
|
|
804
|
+
const commitmentY = nobleToHex(hash.slice(16, 32)).padStart(64, '0')
|
|
805
|
+
|
|
806
|
+
return { commitmentX, commitmentY }
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private async computeNullifier(
|
|
810
|
+
senderSecretField: string,
|
|
811
|
+
intentHashField: string,
|
|
812
|
+
nonceField: string
|
|
813
|
+
): Promise<string> {
|
|
814
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
815
|
+
const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
|
|
816
|
+
|
|
817
|
+
const secretBytes = hexToBytes(senderSecretField.padStart(64, '0'))
|
|
818
|
+
const intentBytes = hexToBytes(intentHashField)
|
|
819
|
+
const nonceBytes = hexToBytes(nonceField.padStart(64, '0'))
|
|
820
|
+
const preimage = new Uint8Array([...secretBytes, ...intentBytes, ...nonceBytes])
|
|
821
|
+
const hash = sha256(preimage)
|
|
822
|
+
|
|
823
|
+
return nobleToHex(hash)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private async computeOutputCommitment(
|
|
827
|
+
outputAmount: bigint,
|
|
828
|
+
outputBlinding: Uint8Array
|
|
829
|
+
): Promise<{ commitmentX: string; commitmentY: string }> {
|
|
830
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
831
|
+
const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
|
|
832
|
+
|
|
833
|
+
const amountBytes = this.bigintToBytes(outputAmount, 8)
|
|
834
|
+
const blindingBytes = outputBlinding.slice(0, 32)
|
|
835
|
+
const preimage = new Uint8Array([...amountBytes, ...blindingBytes])
|
|
836
|
+
const hash = sha256(preimage)
|
|
837
|
+
|
|
838
|
+
const commitmentX = nobleToHex(hash.slice(0, 16)).padStart(64, '0')
|
|
839
|
+
const commitmentY = nobleToHex(hash.slice(16, 32)).padStart(64, '0')
|
|
840
|
+
|
|
841
|
+
return { commitmentX, commitmentY }
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private async computeSolverId(solverSecretField: string): Promise<string> {
|
|
845
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
846
|
+
const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
|
|
847
|
+
|
|
848
|
+
const secretBytes = hexToBytes(solverSecretField.padStart(64, '0'))
|
|
849
|
+
const hash = sha256(secretBytes)
|
|
850
|
+
|
|
851
|
+
return nobleToHex(hash)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private async computeOracleMessageHash(
|
|
855
|
+
recipient: string,
|
|
856
|
+
amount: bigint,
|
|
857
|
+
txHash: string,
|
|
858
|
+
blockNumber: bigint
|
|
859
|
+
): Promise<number[]> {
|
|
860
|
+
const { sha256 } = await import('@noble/hashes/sha256')
|
|
861
|
+
|
|
862
|
+
const recipientBytes = hexToBytes(this.hexToField(recipient))
|
|
863
|
+
const amountBytes = this.bigintToBytes(amount, 8)
|
|
864
|
+
const txHashBytes = hexToBytes(this.hexToField(txHash))
|
|
865
|
+
const blockBytes = this.bigintToBytes(blockNumber, 8)
|
|
866
|
+
|
|
867
|
+
const preimage = new Uint8Array([
|
|
868
|
+
...recipientBytes,
|
|
869
|
+
...amountBytes,
|
|
870
|
+
...txHashBytes,
|
|
871
|
+
...blockBytes,
|
|
872
|
+
])
|
|
873
|
+
const hash = sha256(preimage)
|
|
874
|
+
|
|
875
|
+
return Array.from(hash)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private getPublicKeyCoordinates(privateKey: Uint8Array): PublicKeyCoordinates {
|
|
879
|
+
const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
|
|
880
|
+
const x = Array.from(uncompressedPubKey.slice(1, 33))
|
|
881
|
+
const y = Array.from(uncompressedPubKey.slice(33, 65))
|
|
882
|
+
return { x, y }
|
|
883
|
+
}
|
|
884
|
+
}
|