@sip-protocol/sdk 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.
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Pedersen Commitment Implementation
3
+ *
4
+ * Cryptographically secure Pedersen commitments on secp256k1.
5
+ *
6
+ * ## Security Properties
7
+ *
8
+ * - **Hiding (Computational)**: Cannot determine value from commitment
9
+ * - **Binding (Computational)**: Cannot open commitment to different value
10
+ * - **Homomorphic**: C(v1) + C(v2) = C(v1 + v2) when blindings sum
11
+ *
12
+ * ## Generator H Construction
13
+ *
14
+ * H is constructed using "nothing-up-my-sleeve" (NUMS) method:
15
+ * - Take a well-known string "SIP-PEDERSEN-GENERATOR-H"
16
+ * - Hash it to get x-coordinate candidate
17
+ * - Iterate until we find a valid curve point
18
+ * - This ensures nobody knows the discrete log of H w.r.t. G
19
+ *
20
+ * @see docs/specs/SIP-SPEC.md Section 3.3 - Pedersen Commitment
21
+ */
22
+
23
+ import { secp256k1 } from '@noble/curves/secp256k1'
24
+ import { sha256 } from '@noble/hashes/sha256'
25
+ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'
26
+ import type { HexString } from '@sip-protocol/types'
27
+ import { ValidationError, CryptoError, ErrorCode } from './errors'
28
+ import { isValidHex } from './validation'
29
+
30
+ // ─── Types ───────────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * A Pedersen commitment with associated blinding factor
34
+ */
35
+ export interface PedersenCommitment {
36
+ /**
37
+ * The commitment point C = v*G + r*H (compressed, 33 bytes)
38
+ */
39
+ commitment: HexString
40
+
41
+ /**
42
+ * The blinding factor r (32 bytes, secret)
43
+ * Required to open/verify the commitment
44
+ */
45
+ blinding: HexString
46
+ }
47
+
48
+ /**
49
+ * A commitment point without the blinding factor (for public sharing)
50
+ */
51
+ export interface CommitmentPoint {
52
+ /**
53
+ * The commitment point (compressed, 33 bytes)
54
+ */
55
+ commitment: HexString
56
+ }
57
+
58
+ // ─── Constants ───────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Domain separation tag for H generation
62
+ */
63
+ const H_DOMAIN = 'SIP-PEDERSEN-GENERATOR-H-v1'
64
+
65
+ /**
66
+ * The generator G (secp256k1 base point)
67
+ */
68
+ const G = secp256k1.ProjectivePoint.BASE
69
+
70
+ /**
71
+ * The independent generator H (NUMS point)
72
+ *
73
+ * Constructed via hash-to-curve with nothing-up-my-sleeve string.
74
+ * Nobody knows log_G(H), making the commitment binding.
75
+ */
76
+ const H = generateH()
77
+
78
+ /**
79
+ * The curve order (number of points in the group)
80
+ */
81
+ const CURVE_ORDER = secp256k1.CURVE.n
82
+
83
+ // ─── Generator H Construction ────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Generate the independent generator H using NUMS method
87
+ *
88
+ * This uses a try-and-increment approach:
89
+ * 1. Hash the domain separator to get a candidate x-coordinate
90
+ * 2. Try to lift x to a curve point
91
+ * 3. If it fails, increment counter and retry
92
+ *
93
+ * This is similar to how Zcash generates their Pedersen generators.
94
+ */
95
+ function generateH(): typeof G {
96
+ let counter = 0
97
+
98
+ while (counter < 256) {
99
+ // Create candidate x-coordinate
100
+ const input = new TextEncoder().encode(`${H_DOMAIN}:${counter}`)
101
+ const hashBytes = sha256(input)
102
+
103
+ try {
104
+ // Try to create a point from this x-coordinate (with even y)
105
+ // The '02' prefix indicates compressed point with even y
106
+ const pointBytes = new Uint8Array(33)
107
+ pointBytes[0] = 0x02
108
+ pointBytes.set(hashBytes, 1)
109
+
110
+ // This will throw if not a valid point
111
+ const point = secp256k1.ProjectivePoint.fromHex(pointBytes)
112
+
113
+ // Ensure point is not identity and not G
114
+ if (!point.equals(secp256k1.ProjectivePoint.ZERO) && !point.equals(G)) {
115
+ return point
116
+ }
117
+ } catch {
118
+ // Not a valid point, try next counter
119
+ }
120
+
121
+ counter++
122
+ }
123
+
124
+ // This should never happen with a properly chosen domain separator
125
+ throw new CryptoError(
126
+ 'Failed to generate H point - this should never happen',
127
+ ErrorCode.CRYPTO_FAILED,
128
+ { context: { domain: H_DOMAIN } }
129
+ )
130
+ }
131
+
132
+ // ─── Core Functions ──────────────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Create a Pedersen commitment to a value
136
+ *
137
+ * C = v*G + r*H
138
+ *
139
+ * Where:
140
+ * - v = value (the amount being committed)
141
+ * - r = blinding factor (random, keeps value hidden)
142
+ * - G = base generator
143
+ * - H = independent generator (NUMS)
144
+ *
145
+ * @param value - The value to commit to (must be < curve order)
146
+ * @param blinding - Optional blinding factor (random 32 bytes if not provided)
147
+ * @returns The commitment and blinding factor
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * // Create a commitment to 100 tokens
152
+ * const { commitment, blinding } = commit(100n)
153
+ *
154
+ * // Later, prove the commitment contains 100
155
+ * const valid = verifyOpening(commitment, 100n, blinding)
156
+ * ```
157
+ */
158
+ export function commit(
159
+ value: bigint,
160
+ blinding?: Uint8Array,
161
+ ): PedersenCommitment {
162
+ // Validate value type
163
+ if (typeof value !== 'bigint') {
164
+ throw new ValidationError('must be a bigint', 'value', { received: typeof value })
165
+ }
166
+
167
+ // Validate value is in valid range
168
+ if (value < 0n) {
169
+ throw new ValidationError('must be non-negative', 'value')
170
+ }
171
+ if (value >= CURVE_ORDER) {
172
+ throw new ValidationError(
173
+ 'must be less than curve order',
174
+ 'value',
175
+ { curveOrder: CURVE_ORDER.toString(16) }
176
+ )
177
+ }
178
+
179
+ // Generate or use provided blinding factor
180
+ const r = blinding ?? randomBytes(32)
181
+ if (r.length !== 32) {
182
+ throw new ValidationError('must be 32 bytes', 'blinding', { received: r.length })
183
+ }
184
+
185
+ // Ensure blinding is in valid range (mod n), and non-zero for valid scalar
186
+ let rScalar = bytesToBigInt(r) % CURVE_ORDER
187
+ if (rScalar === 0n) {
188
+ rScalar = 1n // Avoid zero scalar which is invalid
189
+ }
190
+
191
+ // C = v*G + r*H
192
+ // Handle edge cases where value or blinding could be zero
193
+ let C: typeof G
194
+
195
+ if (value === 0n && rScalar === 0n) {
196
+ // Both zero - use identity point (edge case, shouldn't happen with above fix)
197
+ C = secp256k1.ProjectivePoint.ZERO
198
+ } else if (value === 0n) {
199
+ // Only blinding contributes: C = r*H
200
+ C = H.multiply(rScalar)
201
+ } else if (rScalar === 0n) {
202
+ // Only value contributes: C = v*G (shouldn't happen with above fix)
203
+ C = G.multiply(value)
204
+ } else {
205
+ // Normal case: C = v*G + r*H
206
+ const vG = G.multiply(value)
207
+ const rH = H.multiply(rScalar)
208
+ C = vG.add(rH)
209
+ }
210
+
211
+ return {
212
+ commitment: `0x${bytesToHex(C.toRawBytes(true))}` as HexString,
213
+ blinding: `0x${bytesToHex(r)}` as HexString,
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Verify that a commitment opens to a specific value
219
+ *
220
+ * Recomputes C' = v*G + r*H and checks if C' == C
221
+ *
222
+ * @param commitment - The commitment point to verify
223
+ * @param value - The claimed value
224
+ * @param blinding - The blinding factor used
225
+ * @returns true if the commitment opens correctly
226
+ */
227
+ export function verifyOpening(
228
+ commitment: HexString,
229
+ value: bigint,
230
+ blinding: HexString,
231
+ ): boolean {
232
+ try {
233
+ // Handle special case of zero commitment (point at infinity)
234
+ if (commitment === '0x00') {
235
+ // Zero commitment only opens to (0, 0) - but that's not valid with our blinding adjustment
236
+ // Actually, zero point means C = C, so it should verify for 0, 0 blinding
237
+ return value === 0n && blinding === ('0x' + '0'.repeat(64))
238
+ }
239
+
240
+ // Parse the commitment point
241
+ const C = secp256k1.ProjectivePoint.fromHex(commitment.slice(2))
242
+
243
+ // Recompute expected commitment
244
+ const blindingBytes = hexToBytes(blinding.slice(2))
245
+ let rScalar = bytesToBigInt(blindingBytes) % CURVE_ORDER
246
+ if (rScalar === 0n) {
247
+ rScalar = 1n // Match the commit() behavior
248
+ }
249
+
250
+ // Handle edge cases
251
+ let expected: typeof G
252
+ if (value === 0n) {
253
+ expected = H.multiply(rScalar)
254
+ } else if (rScalar === 0n) {
255
+ expected = G.multiply(value)
256
+ } else {
257
+ const vG = G.multiply(value)
258
+ const rH = H.multiply(rScalar)
259
+ expected = vG.add(rH)
260
+ }
261
+
262
+ // Check equality
263
+ return C.equals(expected)
264
+ } catch {
265
+ return false
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Create a commitment to zero with a specific blinding factor
271
+ *
272
+ * C = 0*G + r*H = r*H
273
+ *
274
+ * Useful for creating balance proofs.
275
+ *
276
+ * @param blinding - The blinding factor
277
+ * @returns Commitment to zero
278
+ */
279
+ export function commitZero(blinding: Uint8Array): PedersenCommitment {
280
+ return commit(0n, blinding)
281
+ }
282
+
283
+ // ─── Homomorphic Operations ──────────────────────────────────────────────────
284
+
285
+ /**
286
+ * Add two commitments homomorphically
287
+ *
288
+ * C1 + C2 = (v1*G + r1*H) + (v2*G + r2*H) = (v1+v2)*G + (r1+r2)*H
289
+ *
290
+ * Note: The blinding factors also add. If you need to verify the sum,
291
+ * you must also sum the blinding factors.
292
+ *
293
+ * @param c1 - First commitment point
294
+ * @param c2 - Second commitment point
295
+ * @returns Sum of commitments
296
+ * @throws {ValidationError} If commitments are invalid hex strings
297
+ */
298
+ export function addCommitments(
299
+ c1: HexString,
300
+ c2: HexString,
301
+ ): CommitmentPoint {
302
+ // Validate inputs
303
+ if (!isValidHex(c1)) {
304
+ throw new ValidationError('must be a valid hex string', 'c1')
305
+ }
306
+ if (!isValidHex(c2)) {
307
+ throw new ValidationError('must be a valid hex string', 'c2')
308
+ }
309
+
310
+ let point1: typeof G
311
+ let point2: typeof G
312
+ try {
313
+ point1 = secp256k1.ProjectivePoint.fromHex(c1.slice(2))
314
+ } catch {
315
+ throw new ValidationError('must be a valid curve point', 'c1')
316
+ }
317
+ try {
318
+ point2 = secp256k1.ProjectivePoint.fromHex(c2.slice(2))
319
+ } catch {
320
+ throw new ValidationError('must be a valid curve point', 'c2')
321
+ }
322
+
323
+ const sum = point1.add(point2)
324
+
325
+ return {
326
+ commitment: `0x${bytesToHex(sum.toRawBytes(true))}` as HexString,
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Subtract two commitments homomorphically
332
+ *
333
+ * C1 - C2 = (v1-v2)*G + (r1-r2)*H
334
+ *
335
+ * @param c1 - First commitment point
336
+ * @param c2 - Second commitment point (to subtract)
337
+ * @returns Difference of commitments
338
+ * @throws {ValidationError} If commitments are invalid
339
+ */
340
+ export function subtractCommitments(
341
+ c1: HexString,
342
+ c2: HexString,
343
+ ): CommitmentPoint {
344
+ // Validate inputs
345
+ if (!isValidHex(c1)) {
346
+ throw new ValidationError('must be a valid hex string', 'c1')
347
+ }
348
+ if (!isValidHex(c2)) {
349
+ throw new ValidationError('must be a valid hex string', 'c2')
350
+ }
351
+
352
+ let point1: typeof G
353
+ let point2: typeof G
354
+ try {
355
+ point1 = secp256k1.ProjectivePoint.fromHex(c1.slice(2))
356
+ } catch {
357
+ throw new ValidationError('must be a valid curve point', 'c1')
358
+ }
359
+ try {
360
+ point2 = secp256k1.ProjectivePoint.fromHex(c2.slice(2))
361
+ } catch {
362
+ throw new ValidationError('must be a valid curve point', 'c2')
363
+ }
364
+
365
+ const diff = point1.subtract(point2)
366
+
367
+ // Handle ZERO point (identity element) - can't serialize directly
368
+ if (diff.equals(secp256k1.ProjectivePoint.ZERO)) {
369
+ // Return a special marker for zero commitment
370
+ // This is the point at infinity, represented as all zeros
371
+ return {
372
+ commitment: '0x00' as HexString,
373
+ }
374
+ }
375
+
376
+ return {
377
+ commitment: `0x${bytesToHex(diff.toRawBytes(true))}` as HexString,
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Add blinding factors (for use with homomorphic addition)
383
+ *
384
+ * When you add commitments, the result commits to (v1+v2) with
385
+ * blinding (r1+r2). Use this to compute the combined blinding.
386
+ *
387
+ * @param b1 - First blinding factor
388
+ * @param b2 - Second blinding factor
389
+ * @returns Sum of blindings (mod curve order)
390
+ */
391
+ export function addBlindings(b1: HexString, b2: HexString): HexString {
392
+ const r1 = bytesToBigInt(hexToBytes(b1.slice(2)))
393
+ const r2 = bytesToBigInt(hexToBytes(b2.slice(2)))
394
+
395
+ const sum = (r1 + r2) % CURVE_ORDER
396
+ const sumBytes = bigIntToBytes(sum, 32)
397
+
398
+ return `0x${bytesToHex(sumBytes)}` as HexString
399
+ }
400
+
401
+ /**
402
+ * Subtract blinding factors (for use with homomorphic subtraction)
403
+ *
404
+ * @param b1 - First blinding factor
405
+ * @param b2 - Second blinding factor (to subtract)
406
+ * @returns Difference of blindings (mod curve order)
407
+ */
408
+ export function subtractBlindings(b1: HexString, b2: HexString): HexString {
409
+ const r1 = bytesToBigInt(hexToBytes(b1.slice(2)))
410
+ const r2 = bytesToBigInt(hexToBytes(b2.slice(2)))
411
+
412
+ // Handle underflow with modular arithmetic
413
+ const diff = (r1 - r2 + CURVE_ORDER) % CURVE_ORDER
414
+ const diffBytes = bigIntToBytes(diff, 32)
415
+
416
+ return `0x${bytesToHex(diffBytes)}` as HexString
417
+ }
418
+
419
+ // ─── Range Proof Support ─────────────────────────────────────────────────────
420
+
421
+ /**
422
+ * Get the generators for ZK proof integration
423
+ *
424
+ * Returns the G and H points for use in Noir circuits.
425
+ */
426
+ export function getGenerators(): {
427
+ G: { x: HexString; y: HexString }
428
+ H: { x: HexString; y: HexString }
429
+ } {
430
+ const gAffine = G.toAffine()
431
+ const hAffine = H.toAffine()
432
+
433
+ return {
434
+ G: {
435
+ x: `0x${gAffine.x.toString(16).padStart(64, '0')}` as HexString,
436
+ y: `0x${gAffine.y.toString(16).padStart(64, '0')}` as HexString,
437
+ },
438
+ H: {
439
+ x: `0x${hAffine.x.toString(16).padStart(64, '0')}` as HexString,
440
+ y: `0x${hAffine.y.toString(16).padStart(64, '0')}` as HexString,
441
+ },
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Generate a random blinding factor
447
+ */
448
+ export function generateBlinding(): HexString {
449
+ return `0x${bytesToHex(randomBytes(32))}` as HexString
450
+ }
451
+
452
+ // ─── Utility Functions ───────────────────────────────────────────────────────
453
+
454
+ function bytesToBigInt(bytes: Uint8Array): bigint {
455
+ let result = 0n
456
+ for (const byte of bytes) {
457
+ result = (result << 8n) + BigInt(byte)
458
+ }
459
+ return result
460
+ }
461
+
462
+ function bigIntToBytes(value: bigint, length: number): Uint8Array {
463
+ const bytes = new Uint8Array(length)
464
+ let v = value
465
+ for (let i = length - 1; i >= 0; i--) {
466
+ bytes[i] = Number(v & 0xffn)
467
+ v >>= 8n
468
+ }
469
+ return bytes
470
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cryptographic utilities for SIP Protocol
3
+ *
4
+ * For ZK proofs, use ProofProvider:
5
+ * @see ./proofs/interface.ts for the proof provider interface
6
+ * @see ./proofs/mock.ts for testing
7
+ * @see ./proofs/noir.ts for production (Noir circuits)
8
+ *
9
+ * For Pedersen commitments, use the dedicated commitment module:
10
+ * @see ./commitment.ts for secure Pedersen commitment implementation
11
+ */
12
+
13
+ import { sha256 } from '@noble/hashes/sha256'
14
+ import { bytesToHex, randomBytes } from '@noble/hashes/utils'
15
+ import type { Commitment, HexString, Hash } from '@sip-protocol/types'
16
+ import { commit, verifyOpening } from './commitment'
17
+ import { ValidationError, ErrorCode } from './errors'
18
+
19
+ /**
20
+ * Create a Pedersen commitment to a value
21
+ *
22
+ * @deprecated Use `commit()` from './commitment' for new code.
23
+ * This wrapper maintains backward compatibility.
24
+ *
25
+ * @param value - The value to commit to
26
+ * @param blindingFactor - Optional blinding factor (random if not provided)
27
+ * @returns Commitment object (legacy format)
28
+ */
29
+ export function createCommitment(
30
+ value: bigint,
31
+ blindingFactor?: Uint8Array,
32
+ ): Commitment {
33
+ console.warn(
34
+ 'createCommitment() is deprecated and will be removed in v0.2.0. ' +
35
+ 'Use commit() from "./commitment" instead.'
36
+ )
37
+
38
+ const { commitment, blinding } = commit(value, blindingFactor)
39
+
40
+ return {
41
+ value: commitment,
42
+ blindingFactor: blinding,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Verify a Pedersen commitment (requires knowing the value and blinding factor)
48
+ *
49
+ * @deprecated Use `verifyOpening()` from './commitment' for new code.
50
+ */
51
+ export function verifyCommitment(
52
+ commitment: Commitment,
53
+ expectedValue: bigint,
54
+ ): boolean {
55
+ console.warn(
56
+ 'verifyCommitment() is deprecated and will be removed in v0.2.0. ' +
57
+ 'Use verifyOpening() from "./commitment" instead.'
58
+ )
59
+
60
+ if (!commitment.blindingFactor) {
61
+ throw new ValidationError(
62
+ 'cannot verify commitment without blinding factor',
63
+ 'commitment.blindingFactor',
64
+ undefined,
65
+ ErrorCode.MISSING_REQUIRED
66
+ )
67
+ }
68
+
69
+ return verifyOpening(commitment.value, expectedValue, commitment.blindingFactor)
70
+ }
71
+
72
+ /**
73
+ * Generate a random intent ID
74
+ */
75
+ export function generateIntentId(): string {
76
+ const bytes = randomBytes(16)
77
+ return `sip-${bytesToHex(bytes)}`
78
+ }
79
+
80
+ /**
81
+ * Hash data using SHA256
82
+ */
83
+ export function hash(data: string | Uint8Array): Hash {
84
+ const input = typeof data === 'string' ? new TextEncoder().encode(data) : data
85
+ return `0x${bytesToHex(sha256(input))}` as Hash
86
+ }
87
+
88
+ /**
89
+ * Generate random bytes
90
+ */
91
+ export function generateRandomBytes(length: number): HexString {
92
+ return `0x${bytesToHex(randomBytes(length))}` as HexString
93
+ }