@sip-protocol/sdk 0.6.0 → 0.6.2
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/README.md +58 -0
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +2752 -448
- package/dist/browser.mjs +31 -1
- package/dist/chunk-7QZPORY5.mjs +15604 -0
- package/dist/chunk-C2NPCUAJ.mjs +17010 -0
- package/dist/chunk-FCVLFUIC.mjs +16699 -0
- package/dist/chunk-G5UHXECN.mjs +16340 -0
- package/dist/chunk-GEDEIZHJ.mjs +16798 -0
- package/dist/chunk-GOOEOAMV.mjs +17026 -0
- package/dist/chunk-MTNYSNR7.mjs +16269 -0
- package/dist/chunk-O5PIB2EA.mjs +16698 -0
- package/dist/chunk-PCFM7FQO.mjs +17010 -0
- package/dist/chunk-QK464ARC.mjs +16946 -0
- package/dist/chunk-VNBMNGC3.mjs +16698 -0
- package/dist/chunk-W5TUELDQ.mjs +16947 -0
- package/dist/index-CD_zShu-.d.ts +10870 -0
- package/dist/index-CQBYdLYy.d.mts +10976 -0
- package/dist/index-Cg9TYEPv.d.mts +11321 -0
- package/dist/index-CqZJOO8C.d.mts +11323 -0
- package/dist/index-CywN9Bnp.d.ts +11321 -0
- package/dist/index-DHy5ZjCD.d.ts +10976 -0
- package/dist/index-DfsVsmxu.d.ts +11323 -0
- package/dist/index-ObjwyVDX.d.mts +10870 -0
- package/dist/index-m0xbSfmT.d.mts +11318 -0
- package/dist/index-rWLEgvhN.d.ts +11318 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2737 -427
- package/dist/index.mjs +31 -1
- package/dist/noir-DKfEzWy9.d.mts +482 -0
- package/dist/noir-DKfEzWy9.d.ts +482 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +12 -3
- package/dist/proofs/noir.mjs +12 -3
- package/package.json +16 -14
- package/src/adapters/near-intents.ts +13 -3
- package/src/auction/index.ts +20 -0
- package/src/auction/sealed-bid.ts +1037 -0
- package/src/compliance/derivation.ts +13 -3
- package/src/compliance/reports.ts +5 -4
- package/src/cosmos/ibc-stealth.ts +2 -2
- package/src/cosmos/stealth.ts +2 -2
- package/src/governance/index.ts +19 -0
- package/src/governance/private-vote.ts +1116 -0
- package/src/index.ts +50 -2
- package/src/intent.ts +145 -8
- package/src/nft/index.ts +27 -0
- package/src/nft/private-nft.ts +811 -0
- package/src/proofs/browser-utils.ts +1 -7
- package/src/proofs/noir.ts +34 -7
- package/src/settlement/backends/direct-chain.ts +14 -3
- package/src/stealth.ts +31 -13
- package/src/types/browser.d.ts +67 -0
- package/src/validation.ts +4 -2
- package/src/wallet/bitcoin/adapter.ts +159 -15
- package/src/wallet/bitcoin/types.ts +340 -15
- package/src/wallet/cosmos/mock.ts +16 -12
- package/src/wallet/hardware/ledger.ts +82 -12
- package/src/wallet/hardware/types.ts +2 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealed-Bid Auction Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements cryptographically binding sealed bids using Pedersen commitments.
|
|
5
|
+
* Bidders commit to their bid amounts without revealing them until the reveal phase.
|
|
6
|
+
*
|
|
7
|
+
* ## Use Cases
|
|
8
|
+
*
|
|
9
|
+
* - First-price sealed-bid auctions (pay your bid)
|
|
10
|
+
* - Second-price sealed-bid auctions (Vickrey auctions - pay second-highest bid)
|
|
11
|
+
* - NFT auctions with privacy
|
|
12
|
+
* - Government procurement contracts
|
|
13
|
+
* - Private sales
|
|
14
|
+
*
|
|
15
|
+
* ## Security Properties
|
|
16
|
+
*
|
|
17
|
+
* - **Binding**: Cannot change bid after commitment (cryptographically enforced)
|
|
18
|
+
* - **Hiding**: Bid amount hidden until reveal phase (computational hiding)
|
|
19
|
+
* - **Verifiable**: Anyone can verify revealed bid matches commitment
|
|
20
|
+
* - **Non-malleable**: Commitments use secure randomness (salt)
|
|
21
|
+
*
|
|
22
|
+
* ## Workflow
|
|
23
|
+
*
|
|
24
|
+
* 1. **Bidding Phase**: Create sealed bid with `createBid()`
|
|
25
|
+
* 2. **Submit Phase**: Submit commitment on-chain (external)
|
|
26
|
+
* 3. **Reveal Phase**: Reveal bid amount and salt
|
|
27
|
+
* 4. **Verification**: Anyone can verify with `verifyBid()`
|
|
28
|
+
*
|
|
29
|
+
* @module auction/sealed-bid
|
|
30
|
+
* @see {@link commit} from '../commitment' for underlying commitment scheme
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { commit, verifyOpening, subtractCommitments, subtractBlindings } from '../commitment'
|
|
34
|
+
import { randomBytes, bytesToHex } from '@noble/hashes/utils'
|
|
35
|
+
import { hash } from '../crypto'
|
|
36
|
+
import type { HexString, WinnerResult, WinnerProof, WinnerVerification } from '@sip-protocol/types'
|
|
37
|
+
import { ValidationError } from '../errors'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A sealed bid with cryptographic commitment
|
|
41
|
+
*/
|
|
42
|
+
export interface SealedBid {
|
|
43
|
+
/**
|
|
44
|
+
* Unique identifier for the auction
|
|
45
|
+
*/
|
|
46
|
+
auctionId: string
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pedersen commitment to the bid amount
|
|
50
|
+
* Format: C = amount*G + salt*H (compressed, 33 bytes)
|
|
51
|
+
*/
|
|
52
|
+
commitment: HexString
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unix timestamp when bid was created (milliseconds)
|
|
56
|
+
*/
|
|
57
|
+
timestamp: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Complete bid data including secrets (for bidder's records)
|
|
62
|
+
*/
|
|
63
|
+
export interface BidReceipt extends SealedBid {
|
|
64
|
+
/**
|
|
65
|
+
* The bid amount (secret, don't reveal until reveal phase)
|
|
66
|
+
*/
|
|
67
|
+
amount: bigint
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The salt/blinding factor (secret, needed to open commitment)
|
|
71
|
+
*/
|
|
72
|
+
salt: HexString
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A revealed bid with all information public
|
|
77
|
+
*/
|
|
78
|
+
export interface RevealedBid {
|
|
79
|
+
/**
|
|
80
|
+
* Unique identifier for the auction
|
|
81
|
+
*/
|
|
82
|
+
auctionId: string
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The commitment that was submitted during bidding phase
|
|
86
|
+
*/
|
|
87
|
+
commitment: HexString
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The revealed bid amount
|
|
91
|
+
*/
|
|
92
|
+
amount: bigint
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The revealed salt used in the commitment
|
|
96
|
+
*/
|
|
97
|
+
salt: HexString
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unix timestamp when bid was originally created (milliseconds)
|
|
101
|
+
*/
|
|
102
|
+
timestamp: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parameters for creating a sealed bid
|
|
107
|
+
*/
|
|
108
|
+
export interface CreateBidParams {
|
|
109
|
+
/**
|
|
110
|
+
* Unique identifier for the auction
|
|
111
|
+
*/
|
|
112
|
+
auctionId: string
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Bid amount in smallest units (e.g., wei for ETH)
|
|
116
|
+
* Must be positive
|
|
117
|
+
*/
|
|
118
|
+
amount: bigint
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Optional custom salt for commitment
|
|
122
|
+
* If not provided, secure random bytes are generated
|
|
123
|
+
* Must be 32 bytes if provided
|
|
124
|
+
*/
|
|
125
|
+
salt?: Uint8Array
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parameters for verifying a revealed bid
|
|
130
|
+
*/
|
|
131
|
+
export interface VerifyBidParams {
|
|
132
|
+
/**
|
|
133
|
+
* The commitment to verify
|
|
134
|
+
*/
|
|
135
|
+
commitment: HexString
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The revealed bid amount
|
|
139
|
+
*/
|
|
140
|
+
amount: bigint
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The revealed salt
|
|
144
|
+
*/
|
|
145
|
+
salt: HexString
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sealed-Bid Auction Manager
|
|
150
|
+
*
|
|
151
|
+
* Provides cryptographic primitives for sealed-bid auctions where bidders
|
|
152
|
+
* commit to their bids without revealing them until a reveal phase.
|
|
153
|
+
*
|
|
154
|
+
* @example Basic auction workflow
|
|
155
|
+
* ```typescript
|
|
156
|
+
* import { SealedBidAuction } from '@sip-protocol/sdk'
|
|
157
|
+
*
|
|
158
|
+
* const auction = new SealedBidAuction()
|
|
159
|
+
*
|
|
160
|
+
* // BIDDING PHASE
|
|
161
|
+
* // Alice creates a sealed bid for 100 ETH
|
|
162
|
+
* const aliceBid = auction.createBid({
|
|
163
|
+
* auctionId: 'auction-123',
|
|
164
|
+
* amount: 100n * 10n**18n, // 100 ETH
|
|
165
|
+
* })
|
|
166
|
+
*
|
|
167
|
+
* // Submit commitment on-chain (only commitment is public)
|
|
168
|
+
* await submitBidOnChain({
|
|
169
|
+
* auctionId: aliceBid.auctionId,
|
|
170
|
+
* commitment: aliceBid.commitment,
|
|
171
|
+
* timestamp: aliceBid.timestamp,
|
|
172
|
+
* })
|
|
173
|
+
*
|
|
174
|
+
* // Alice keeps the receipt secret
|
|
175
|
+
* secureStorage.save(aliceBid) // Contains amount + salt
|
|
176
|
+
*
|
|
177
|
+
* // REVEAL PHASE (after bidding closes)
|
|
178
|
+
* // Alice reveals her bid
|
|
179
|
+
* await revealBidOnChain({
|
|
180
|
+
* auctionId: aliceBid.auctionId,
|
|
181
|
+
* amount: aliceBid.amount,
|
|
182
|
+
* salt: aliceBid.salt,
|
|
183
|
+
* })
|
|
184
|
+
*
|
|
185
|
+
* // Anyone can verify the revealed bid matches the commitment
|
|
186
|
+
* const isValid = auction.verifyBid({
|
|
187
|
+
* commitment: aliceBid.commitment,
|
|
188
|
+
* amount: aliceBid.amount,
|
|
189
|
+
* salt: aliceBid.salt,
|
|
190
|
+
* })
|
|
191
|
+
* console.log('Bid valid:', isValid) // true
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example Multiple bidders
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // Bob and Carol also bid
|
|
197
|
+
* const bobBid = auction.createBid({
|
|
198
|
+
* auctionId: 'auction-123',
|
|
199
|
+
* amount: 150n * 10n**18n, // 150 ETH
|
|
200
|
+
* })
|
|
201
|
+
*
|
|
202
|
+
* const carolBid = auction.createBid({
|
|
203
|
+
* auctionId: 'auction-123',
|
|
204
|
+
* amount: 120n * 10n**18n, // 120 ETH
|
|
205
|
+
* })
|
|
206
|
+
*
|
|
207
|
+
* // All commitments are public, but amounts are hidden
|
|
208
|
+
* // Nobody knows who bid what until reveal phase
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @example Vickrey auction (second-price)
|
|
212
|
+
* ```typescript
|
|
213
|
+
* // After all bids revealed, determine winner
|
|
214
|
+
* const bids = [
|
|
215
|
+
* { bidder: 'Alice', amount: 100n },
|
|
216
|
+
* { bidder: 'Bob', amount: 150n }, // Highest bid
|
|
217
|
+
* { bidder: 'Carol', amount: 120n }, // Second highest
|
|
218
|
+
* ]
|
|
219
|
+
*
|
|
220
|
+
* const winner = 'Bob' // Highest bidder
|
|
221
|
+
* const price = 120n // Pays second-highest bid (Carol's)
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export class SealedBidAuction {
|
|
225
|
+
/**
|
|
226
|
+
* Create a sealed bid for an auction
|
|
227
|
+
*
|
|
228
|
+
* Generates a cryptographically binding commitment to a bid amount.
|
|
229
|
+
* The commitment can be published publicly without revealing the bid.
|
|
230
|
+
*
|
|
231
|
+
* **Important:** Keep the returned `BidReceipt` secret! It contains the bid
|
|
232
|
+
* amount and salt needed to reveal the bid later. Only publish the commitment.
|
|
233
|
+
*
|
|
234
|
+
* @param params - Bid creation parameters
|
|
235
|
+
* @returns Complete bid receipt (keep secret!) and sealed bid (publish this)
|
|
236
|
+
* @throws {ValidationError} If auctionId is empty, amount is non-positive, or salt is invalid
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const auction = new SealedBidAuction()
|
|
241
|
+
*
|
|
242
|
+
* // Create a bid for 50 ETH
|
|
243
|
+
* const receipt = auction.createBid({
|
|
244
|
+
* auctionId: 'auction-xyz',
|
|
245
|
+
* amount: 50n * 10n**18n,
|
|
246
|
+
* })
|
|
247
|
+
*
|
|
248
|
+
* // Public data (safe to publish)
|
|
249
|
+
* console.log({
|
|
250
|
+
* auctionId: receipt.auctionId,
|
|
251
|
+
* commitment: receipt.commitment,
|
|
252
|
+
* timestamp: receipt.timestamp,
|
|
253
|
+
* })
|
|
254
|
+
*
|
|
255
|
+
* // Secret data (store securely, needed for reveal)
|
|
256
|
+
* secureStorage.save({
|
|
257
|
+
* amount: receipt.amount,
|
|
258
|
+
* salt: receipt.salt,
|
|
259
|
+
* })
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
createBid(params: CreateBidParams): BidReceipt {
|
|
263
|
+
// Validate auction ID
|
|
264
|
+
if (typeof params.auctionId !== 'string' || params.auctionId.length === 0) {
|
|
265
|
+
throw new ValidationError(
|
|
266
|
+
'auctionId must be a non-empty string',
|
|
267
|
+
'auctionId',
|
|
268
|
+
{ received: params.auctionId }
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Validate amount
|
|
273
|
+
if (typeof params.amount !== 'bigint') {
|
|
274
|
+
throw new ValidationError(
|
|
275
|
+
'amount must be a bigint',
|
|
276
|
+
'amount',
|
|
277
|
+
{ received: typeof params.amount }
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (params.amount <= 0n) {
|
|
282
|
+
throw new ValidationError(
|
|
283
|
+
'amount must be positive',
|
|
284
|
+
'amount',
|
|
285
|
+
{ received: params.amount.toString() }
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate salt if provided
|
|
290
|
+
if (params.salt !== undefined) {
|
|
291
|
+
if (!(params.salt instanceof Uint8Array)) {
|
|
292
|
+
throw new ValidationError(
|
|
293
|
+
'salt must be a Uint8Array',
|
|
294
|
+
'salt',
|
|
295
|
+
{ received: typeof params.salt }
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (params.salt.length !== 32) {
|
|
300
|
+
throw new ValidationError(
|
|
301
|
+
'salt must be exactly 32 bytes',
|
|
302
|
+
'salt',
|
|
303
|
+
{ received: params.salt.length }
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Generate or use provided salt
|
|
309
|
+
const salt = params.salt ?? randomBytes(32)
|
|
310
|
+
|
|
311
|
+
// Create Pedersen commitment: C = amount*G + salt*H
|
|
312
|
+
const { commitment, blinding } = commit(params.amount, salt)
|
|
313
|
+
|
|
314
|
+
// Create sealed bid
|
|
315
|
+
const sealedBid: SealedBid = {
|
|
316
|
+
auctionId: params.auctionId,
|
|
317
|
+
commitment,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Return complete receipt with secrets
|
|
322
|
+
return {
|
|
323
|
+
...sealedBid,
|
|
324
|
+
amount: params.amount,
|
|
325
|
+
salt: blinding,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Verify that a revealed bid matches its commitment
|
|
331
|
+
*
|
|
332
|
+
* Checks that the commitment opens to the claimed bid amount with the provided salt.
|
|
333
|
+
* This proves the bidder committed to this exact amount during the bidding phase.
|
|
334
|
+
*
|
|
335
|
+
* @param params - Verification parameters
|
|
336
|
+
* @returns true if the bid is valid, false otherwise
|
|
337
|
+
* @throws {ValidationError} If commitment or salt format is invalid
|
|
338
|
+
*
|
|
339
|
+
* @example Verify a revealed bid
|
|
340
|
+
* ```typescript
|
|
341
|
+
* const auction = new SealedBidAuction()
|
|
342
|
+
*
|
|
343
|
+
* // During reveal phase, bidder reveals their bid
|
|
344
|
+
* const revealed = {
|
|
345
|
+
* commitment: '0x02abc...', // From bidding phase
|
|
346
|
+
* amount: 50n * 10n**18n, // Revealed now
|
|
347
|
+
* salt: '0x123...', // Revealed now
|
|
348
|
+
* }
|
|
349
|
+
*
|
|
350
|
+
* // Anyone can verify
|
|
351
|
+
* const isValid = auction.verifyBid(revealed)
|
|
352
|
+
*
|
|
353
|
+
* if (isValid) {
|
|
354
|
+
* console.log('✓ Bid is valid - bidder committed to this amount')
|
|
355
|
+
* } else {
|
|
356
|
+
* console.log('✗ Bid is invalid - possible cheating attempt!')
|
|
357
|
+
* }
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* @example Detect cheating
|
|
361
|
+
* ```typescript
|
|
362
|
+
* // Bidder tries to change their bid amount
|
|
363
|
+
* const cheatingAttempt = {
|
|
364
|
+
* commitment: aliceBid.commitment, // Original commitment
|
|
365
|
+
* amount: 200n * 10n**18n, // Different amount!
|
|
366
|
+
* salt: aliceBid.salt,
|
|
367
|
+
* }
|
|
368
|
+
*
|
|
369
|
+
* const isValid = auction.verifyBid(cheatingAttempt)
|
|
370
|
+
* console.log(isValid) // false - commitment doesn't match!
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
verifyBid(params: VerifyBidParams): boolean {
|
|
374
|
+
// Validate commitment format
|
|
375
|
+
if (typeof params.commitment !== 'string' || !params.commitment.startsWith('0x')) {
|
|
376
|
+
throw new ValidationError(
|
|
377
|
+
'commitment must be a hex string with 0x prefix',
|
|
378
|
+
'commitment',
|
|
379
|
+
{ received: params.commitment }
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Validate amount
|
|
384
|
+
if (typeof params.amount !== 'bigint') {
|
|
385
|
+
throw new ValidationError(
|
|
386
|
+
'amount must be a bigint',
|
|
387
|
+
'amount',
|
|
388
|
+
{ received: typeof params.amount }
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Validate salt format
|
|
393
|
+
if (typeof params.salt !== 'string' || !params.salt.startsWith('0x')) {
|
|
394
|
+
throw new ValidationError(
|
|
395
|
+
'salt must be a hex string with 0x prefix',
|
|
396
|
+
'salt',
|
|
397
|
+
{ received: params.salt }
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Verify the commitment opens to the claimed amount
|
|
402
|
+
return verifyOpening(params.commitment, params.amount, params.salt)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Reveal a sealed bid by exposing the amount and salt
|
|
407
|
+
*
|
|
408
|
+
* Converts a BidReceipt (with secrets) into a RevealedBid (all public).
|
|
409
|
+
* This is what bidders submit during the reveal phase to prove their bid.
|
|
410
|
+
*
|
|
411
|
+
* **Important:** This method validates that the revealed data matches the
|
|
412
|
+
* commitment before returning. If validation fails, it throws an error.
|
|
413
|
+
*
|
|
414
|
+
* @param bid - The sealed bid to reveal (must include amount and salt from BidReceipt)
|
|
415
|
+
* @param amount - The bid amount to reveal
|
|
416
|
+
* @param salt - The salt/blinding factor to reveal
|
|
417
|
+
* @returns Complete revealed bid ready for public verification
|
|
418
|
+
* @throws {ValidationError} If the revealed data doesn't match the commitment (cheating attempt)
|
|
419
|
+
*
|
|
420
|
+
* @example Reveal a bid during reveal phase
|
|
421
|
+
* ```typescript
|
|
422
|
+
* const auction = new SealedBidAuction()
|
|
423
|
+
*
|
|
424
|
+
* // BIDDING PHASE
|
|
425
|
+
* const receipt = auction.createBid({
|
|
426
|
+
* auctionId: 'auction-1',
|
|
427
|
+
* amount: 100n,
|
|
428
|
+
* })
|
|
429
|
+
*
|
|
430
|
+
* // Submit commitment on-chain (only commitment is public)
|
|
431
|
+
* await submitToChain({
|
|
432
|
+
* auctionId: receipt.auctionId,
|
|
433
|
+
* commitment: receipt.commitment,
|
|
434
|
+
* timestamp: receipt.timestamp,
|
|
435
|
+
* })
|
|
436
|
+
*
|
|
437
|
+
* // REVEAL PHASE (after bidding closes)
|
|
438
|
+
* const revealed = auction.revealBid(
|
|
439
|
+
* { auctionId: receipt.auctionId, commitment: receipt.commitment, timestamp: receipt.timestamp },
|
|
440
|
+
* receipt.amount,
|
|
441
|
+
* receipt.salt
|
|
442
|
+
* )
|
|
443
|
+
*
|
|
444
|
+
* // Submit revealed bid on-chain for verification
|
|
445
|
+
* await revealOnChain(revealed)
|
|
446
|
+
* ```
|
|
447
|
+
*
|
|
448
|
+
* @example Detect invalid reveal attempt
|
|
449
|
+
* ```typescript
|
|
450
|
+
* const receipt = auction.createBid({
|
|
451
|
+
* auctionId: 'auction-1',
|
|
452
|
+
* amount: 100n,
|
|
453
|
+
* })
|
|
454
|
+
*
|
|
455
|
+
* // Try to reveal a different amount (cheating!)
|
|
456
|
+
* try {
|
|
457
|
+
* auction.revealBid(
|
|
458
|
+
* { auctionId: receipt.auctionId, commitment: receipt.commitment, timestamp: receipt.timestamp },
|
|
459
|
+
* 200n, // Different amount!
|
|
460
|
+
* receipt.salt
|
|
461
|
+
* )
|
|
462
|
+
* } catch (error) {
|
|
463
|
+
* console.log('Cheating detected!') // ValidationError thrown
|
|
464
|
+
* }
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
revealBid(
|
|
468
|
+
bid: SealedBid,
|
|
469
|
+
amount: bigint,
|
|
470
|
+
salt: Uint8Array,
|
|
471
|
+
): RevealedBid {
|
|
472
|
+
// Convert salt to hex if needed
|
|
473
|
+
const saltHex = `0x${bytesToHex(salt)}` as HexString
|
|
474
|
+
|
|
475
|
+
// Validate that the reveal matches the commitment
|
|
476
|
+
const isValid = this.verifyBid({
|
|
477
|
+
commitment: bid.commitment,
|
|
478
|
+
amount,
|
|
479
|
+
salt: saltHex,
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
if (!isValid) {
|
|
483
|
+
throw new ValidationError(
|
|
484
|
+
'revealed bid does not match commitment - possible cheating attempt',
|
|
485
|
+
'reveal',
|
|
486
|
+
{
|
|
487
|
+
commitment: bid.commitment,
|
|
488
|
+
amount: amount.toString(),
|
|
489
|
+
expectedMatch: true,
|
|
490
|
+
actualMatch: false,
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Create and return the revealed bid
|
|
496
|
+
return {
|
|
497
|
+
auctionId: bid.auctionId,
|
|
498
|
+
commitment: bid.commitment,
|
|
499
|
+
amount,
|
|
500
|
+
salt: saltHex,
|
|
501
|
+
timestamp: bid.timestamp,
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Verify that a revealed bid matches its original sealed bid
|
|
507
|
+
*
|
|
508
|
+
* Convenience method that verifies a RevealedBid object.
|
|
509
|
+
* This is equivalent to calling verifyBid() with the reveal's components.
|
|
510
|
+
*
|
|
511
|
+
* @param bid - The sealed bid from the bidding phase
|
|
512
|
+
* @param reveal - The revealed bid to verify
|
|
513
|
+
* @returns true if reveal is valid, false otherwise
|
|
514
|
+
* @throws {ValidationError} If inputs are malformed
|
|
515
|
+
*
|
|
516
|
+
* @example Verify a revealed bid
|
|
517
|
+
* ```typescript
|
|
518
|
+
* const auction = new SealedBidAuction()
|
|
519
|
+
*
|
|
520
|
+
* // Bidding phase
|
|
521
|
+
* const receipt = auction.createBid({
|
|
522
|
+
* auctionId: 'auction-1',
|
|
523
|
+
* amount: 100n,
|
|
524
|
+
* })
|
|
525
|
+
*
|
|
526
|
+
* const sealedBid = {
|
|
527
|
+
* auctionId: receipt.auctionId,
|
|
528
|
+
* commitment: receipt.commitment,
|
|
529
|
+
* timestamp: receipt.timestamp,
|
|
530
|
+
* }
|
|
531
|
+
*
|
|
532
|
+
* // Reveal phase
|
|
533
|
+
* const reveal = auction.revealBid(sealedBid, receipt.amount, hexToBytes(receipt.salt.slice(2)))
|
|
534
|
+
*
|
|
535
|
+
* // Anyone can verify
|
|
536
|
+
* const isValid = auction.verifyReveal(sealedBid, reveal)
|
|
537
|
+
* console.log(isValid) // true
|
|
538
|
+
* ```
|
|
539
|
+
*
|
|
540
|
+
* @example Detect mismatched reveal
|
|
541
|
+
* ```typescript
|
|
542
|
+
* // Someone tries to reveal a different bid for the same commitment
|
|
543
|
+
* const fakeReveal = {
|
|
544
|
+
* ...reveal,
|
|
545
|
+
* amount: 200n, // Different amount!
|
|
546
|
+
* }
|
|
547
|
+
*
|
|
548
|
+
* const isValid = auction.verifyReveal(sealedBid, fakeReveal)
|
|
549
|
+
* console.log(isValid) // false
|
|
550
|
+
* ```
|
|
551
|
+
*/
|
|
552
|
+
verifyReveal(
|
|
553
|
+
bid: SealedBid,
|
|
554
|
+
reveal: RevealedBid,
|
|
555
|
+
): boolean {
|
|
556
|
+
// Verify auction IDs match
|
|
557
|
+
if (bid.auctionId !== reveal.auctionId) {
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Verify commitments match
|
|
562
|
+
if (bid.commitment !== reveal.commitment) {
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Verify the cryptographic opening
|
|
567
|
+
return this.verifyBid({
|
|
568
|
+
commitment: reveal.commitment,
|
|
569
|
+
amount: reveal.amount,
|
|
570
|
+
salt: reveal.salt,
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Hash auction metadata for deterministic auction IDs
|
|
576
|
+
*
|
|
577
|
+
* Creates a unique auction identifier from auction parameters.
|
|
578
|
+
* Useful for creating verifiable auction IDs that commit to the auction rules.
|
|
579
|
+
*
|
|
580
|
+
* @param data - Auction metadata to hash
|
|
581
|
+
* @returns Hex-encoded hash of the auction metadata
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* const auction = new SealedBidAuction()
|
|
586
|
+
*
|
|
587
|
+
* // Create deterministic auction ID
|
|
588
|
+
* const auctionId = auction.hashAuctionMetadata({
|
|
589
|
+
* itemId: 'nft-token-123',
|
|
590
|
+
* seller: '0xABCD...',
|
|
591
|
+
* startTime: 1704067200,
|
|
592
|
+
* endTime: 1704153600,
|
|
593
|
+
* })
|
|
594
|
+
*
|
|
595
|
+
* // Use this ID for all bids
|
|
596
|
+
* const bid = auction.createBid({
|
|
597
|
+
* auctionId,
|
|
598
|
+
* amount: 100n,
|
|
599
|
+
* })
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
hashAuctionMetadata(data: Record<string, unknown>): HexString {
|
|
603
|
+
const jsonString = JSON.stringify(data, (_, value) =>
|
|
604
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
605
|
+
)
|
|
606
|
+
return hash(jsonString)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Determine the winner from revealed bids
|
|
611
|
+
*
|
|
612
|
+
* Finds the highest valid bid. In case of tie (same amount), the earliest
|
|
613
|
+
* bid (lowest timestamp) wins.
|
|
614
|
+
*
|
|
615
|
+
* **Important:** This method assumes all bids have been verified as valid
|
|
616
|
+
* (matching their commitments). Always verify bids before determining winner.
|
|
617
|
+
*
|
|
618
|
+
* @param revealedBids - Array of revealed bids to evaluate
|
|
619
|
+
* @returns Winner result with bid details
|
|
620
|
+
* @throws {ValidationError} If no bids provided or auction IDs don't match
|
|
621
|
+
*
|
|
622
|
+
* @example Basic winner determination
|
|
623
|
+
* ```typescript
|
|
624
|
+
* const auction = new SealedBidAuction()
|
|
625
|
+
*
|
|
626
|
+
* // After reveal phase, determine winner
|
|
627
|
+
* const revealedBids = [
|
|
628
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 },
|
|
629
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 150n, salt: '0x...', timestamp: 2000 },
|
|
630
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 120n, salt: '0x...', timestamp: 1500 },
|
|
631
|
+
* ]
|
|
632
|
+
*
|
|
633
|
+
* const winner = auction.determineWinner(revealedBids)
|
|
634
|
+
* console.log(`Winner bid: ${winner.amount} (timestamp: ${winner.timestamp})`)
|
|
635
|
+
* // Output: "Winner bid: 150 (timestamp: 2000)"
|
|
636
|
+
* ```
|
|
637
|
+
*
|
|
638
|
+
* @example Tie-breaking by timestamp
|
|
639
|
+
* ```typescript
|
|
640
|
+
* const tiedBids = [
|
|
641
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 2000 },
|
|
642
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 }, // Earlier
|
|
643
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1500 },
|
|
644
|
+
* ]
|
|
645
|
+
*
|
|
646
|
+
* const winner = auction.determineWinner(tiedBids)
|
|
647
|
+
* console.log(winner.timestamp) // 1000 (earliest bid wins)
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
determineWinner(revealedBids: RevealedBid[]): WinnerResult {
|
|
651
|
+
// Validate inputs
|
|
652
|
+
if (!Array.isArray(revealedBids) || revealedBids.length === 0) {
|
|
653
|
+
throw new ValidationError(
|
|
654
|
+
'revealedBids must be a non-empty array',
|
|
655
|
+
'revealedBids',
|
|
656
|
+
{ received: revealedBids }
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Verify all bids are for the same auction
|
|
661
|
+
const auctionId = revealedBids[0].auctionId
|
|
662
|
+
const mismatchedBid = revealedBids.find(bid => bid.auctionId !== auctionId)
|
|
663
|
+
if (mismatchedBid) {
|
|
664
|
+
throw new ValidationError(
|
|
665
|
+
'all bids must be for the same auction',
|
|
666
|
+
'auctionId',
|
|
667
|
+
{ expected: auctionId, received: mismatchedBid.auctionId }
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Find highest bid, with tie-breaking by earliest timestamp
|
|
672
|
+
let winnerIndex = 0
|
|
673
|
+
let winner = revealedBids[0]
|
|
674
|
+
|
|
675
|
+
for (let i = 1; i < revealedBids.length; i++) {
|
|
676
|
+
const current = revealedBids[i]
|
|
677
|
+
|
|
678
|
+
// Higher amount wins
|
|
679
|
+
if (current.amount > winner.amount) {
|
|
680
|
+
winner = current
|
|
681
|
+
winnerIndex = i
|
|
682
|
+
}
|
|
683
|
+
// Tie: earlier timestamp wins
|
|
684
|
+
else if (current.amount === winner.amount && current.timestamp < winner.timestamp) {
|
|
685
|
+
winner = current
|
|
686
|
+
winnerIndex = i
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
auctionId: winner.auctionId,
|
|
692
|
+
commitment: winner.commitment,
|
|
693
|
+
amount: winner.amount,
|
|
694
|
+
salt: winner.salt,
|
|
695
|
+
timestamp: winner.timestamp,
|
|
696
|
+
bidIndex: winnerIndex,
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Verify that a claimed winner is actually the highest bidder
|
|
702
|
+
*
|
|
703
|
+
* Checks that the winner's amount is >= all other revealed bids.
|
|
704
|
+
* This is a simple verification that requires all bid amounts to be revealed.
|
|
705
|
+
*
|
|
706
|
+
* For privacy-preserving verification (without revealing losing bids),
|
|
707
|
+
* use {@link verifyWinnerProof} instead.
|
|
708
|
+
*
|
|
709
|
+
* @param winner - The claimed winner result
|
|
710
|
+
* @param revealedBids - All revealed bids to check against
|
|
711
|
+
* @returns true if winner is valid, false otherwise
|
|
712
|
+
*
|
|
713
|
+
* @example Verify honest winner
|
|
714
|
+
* ```typescript
|
|
715
|
+
* const auction = new SealedBidAuction()
|
|
716
|
+
*
|
|
717
|
+
* const bids = [
|
|
718
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 },
|
|
719
|
+
* { auctionId: 'auction-1', commitment: '0x...', amount: 150n, salt: '0x...', timestamp: 2000 },
|
|
720
|
+
* ]
|
|
721
|
+
*
|
|
722
|
+
* const winner = auction.determineWinner(bids)
|
|
723
|
+
* const isValid = auction.verifyWinner(winner, bids)
|
|
724
|
+
* console.log(isValid) // true
|
|
725
|
+
* ```
|
|
726
|
+
*
|
|
727
|
+
* @example Detect invalid winner
|
|
728
|
+
* ```typescript
|
|
729
|
+
* // Someone tries to claim they won with a lower bid
|
|
730
|
+
* const fakeWinner = {
|
|
731
|
+
* auctionId: 'auction-1',
|
|
732
|
+
* commitment: '0x...',
|
|
733
|
+
* amount: 50n, // Lower than highest bid!
|
|
734
|
+
* salt: '0x...',
|
|
735
|
+
* timestamp: 500,
|
|
736
|
+
* }
|
|
737
|
+
*
|
|
738
|
+
* const isValid = auction.verifyWinner(fakeWinner, bids)
|
|
739
|
+
* console.log(isValid) // false
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
verifyWinner(winner: WinnerResult, revealedBids: RevealedBid[]): boolean {
|
|
743
|
+
try {
|
|
744
|
+
// Validate inputs
|
|
745
|
+
if (!winner || !revealedBids || revealedBids.length === 0) {
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Verify auction IDs match
|
|
750
|
+
if (!revealedBids.every(bid => bid.auctionId === winner.auctionId)) {
|
|
751
|
+
return false
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Find the winner in the revealed bids
|
|
755
|
+
const winnerBid = revealedBids.find(bid => bid.commitment === winner.commitment)
|
|
756
|
+
if (!winnerBid) {
|
|
757
|
+
return false
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Verify winner's data matches
|
|
761
|
+
if (winnerBid.amount !== winner.amount || winnerBid.salt !== winner.salt) {
|
|
762
|
+
return false
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check that winner amount >= all other bids
|
|
766
|
+
for (const bid of revealedBids) {
|
|
767
|
+
if (bid.amount > winner.amount) {
|
|
768
|
+
return false
|
|
769
|
+
}
|
|
770
|
+
// If tied, check timestamp (earlier wins)
|
|
771
|
+
if (bid.amount === winner.amount && bid.timestamp < winner.timestamp) {
|
|
772
|
+
return false
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return true
|
|
777
|
+
} catch {
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Create a zero-knowledge style proof that a winner is valid
|
|
784
|
+
*
|
|
785
|
+
* Generates a proof that the winner bid is >= all other bids WITHOUT
|
|
786
|
+
* revealing the losing bid amounts. Uses differential commitments to
|
|
787
|
+
* prove relationships between commitments.
|
|
788
|
+
*
|
|
789
|
+
* **Privacy Properties:**
|
|
790
|
+
* - Reveals: Winner amount, number of bids, commitment hash
|
|
791
|
+
* - Hides: All losing bid amounts (they remain committed)
|
|
792
|
+
*
|
|
793
|
+
* **How it works:**
|
|
794
|
+
* For each losing bid i, we compute: C_winner - C_i
|
|
795
|
+
* This differential commitment commits to (amount_winner - amount_i).
|
|
796
|
+
* Observers can verify C_winner - C_i without learning amount_i.
|
|
797
|
+
*
|
|
798
|
+
* @param winner - The winner to create proof for
|
|
799
|
+
* @param revealedBids - All bids (needed to compute differentials)
|
|
800
|
+
* @returns Winner proof ready for verification
|
|
801
|
+
* @throws {ValidationError} If inputs are invalid
|
|
802
|
+
*
|
|
803
|
+
* @example Create winner proof
|
|
804
|
+
* ```typescript
|
|
805
|
+
* const auction = new SealedBidAuction()
|
|
806
|
+
*
|
|
807
|
+
* // After determining winner
|
|
808
|
+
* const bids = [
|
|
809
|
+
* { auctionId: 'auction-1', commitment: '0xabc...', amount: 100n, salt: '0x...', timestamp: 1000 },
|
|
810
|
+
* { auctionId: 'auction-1', commitment: '0xdef...', amount: 150n, salt: '0x...', timestamp: 2000 },
|
|
811
|
+
* { auctionId: 'auction-1', commitment: '0x123...', amount: 120n, salt: '0x...', timestamp: 1500 },
|
|
812
|
+
* ]
|
|
813
|
+
*
|
|
814
|
+
* const winner = auction.determineWinner(bids)
|
|
815
|
+
* const proof = auction.createWinnerProof(winner, bids)
|
|
816
|
+
*
|
|
817
|
+
* // Proof can be verified without revealing losing bids
|
|
818
|
+
* // Only winner amount (150) is revealed
|
|
819
|
+
* console.log(proof.winnerAmount) // 150n
|
|
820
|
+
* console.log(proof.totalBids) // 3
|
|
821
|
+
* console.log(proof.differentialCommitments.length) // 2 (for the 2 losing bids)
|
|
822
|
+
* ```
|
|
823
|
+
*/
|
|
824
|
+
createWinnerProof(winner: WinnerResult, revealedBids: RevealedBid[]): WinnerProof {
|
|
825
|
+
// Validate inputs
|
|
826
|
+
if (!winner || !revealedBids || revealedBids.length === 0) {
|
|
827
|
+
throw new ValidationError(
|
|
828
|
+
'winner and revealedBids are required',
|
|
829
|
+
'createWinnerProof',
|
|
830
|
+
{ winner, bidsCount: revealedBids?.length }
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Verify winner is actually valid
|
|
835
|
+
if (!this.verifyWinner(winner, revealedBids)) {
|
|
836
|
+
throw new ValidationError(
|
|
837
|
+
'winner is not valid - cannot create proof for invalid winner',
|
|
838
|
+
'winner',
|
|
839
|
+
{ winnerAmount: winner.amount.toString() }
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Compute hash of all commitments (sorted for consistency)
|
|
844
|
+
const sortedCommitments = revealedBids
|
|
845
|
+
.map(bid => bid.commitment)
|
|
846
|
+
.sort()
|
|
847
|
+
const commitmentsHash = hash(sortedCommitments.join(','))
|
|
848
|
+
|
|
849
|
+
// Compute differential commitments: C_winner - C_i for each non-winner bid
|
|
850
|
+
const differentialCommitments: HexString[] = []
|
|
851
|
+
|
|
852
|
+
for (const bid of revealedBids) {
|
|
853
|
+
// Skip the winner itself
|
|
854
|
+
if (bid.commitment === winner.commitment) {
|
|
855
|
+
continue
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Compute C_winner - C_bid
|
|
859
|
+
// This commits to (winner_amount - bid_amount) with blinding (winner_r - bid_r)
|
|
860
|
+
const diff = subtractCommitments(winner.commitment, bid.commitment)
|
|
861
|
+
differentialCommitments.push(diff.commitment)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
auctionId: winner.auctionId,
|
|
866
|
+
winnerCommitment: winner.commitment,
|
|
867
|
+
winnerAmount: winner.amount,
|
|
868
|
+
totalBids: revealedBids.length,
|
|
869
|
+
commitmentsHash,
|
|
870
|
+
differentialCommitments,
|
|
871
|
+
timestamp: winner.timestamp,
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Verify a winner proof without revealing losing bid amounts
|
|
877
|
+
*
|
|
878
|
+
* Verifies that the winner proof is valid by checking:
|
|
879
|
+
* 1. Commitments hash matches (prevents tampering)
|
|
880
|
+
* 2. Differential commitments are consistent
|
|
881
|
+
* 3. Winner commitment is included in the original commitments
|
|
882
|
+
*
|
|
883
|
+
* **Privacy:** This verification does NOT require revealing losing bid amounts!
|
|
884
|
+
* Observers only see the winner amount and the differential commitments.
|
|
885
|
+
*
|
|
886
|
+
* @param proof - The winner proof to verify
|
|
887
|
+
* @param allCommitments - All bid commitments (public, from bidding phase)
|
|
888
|
+
* @returns Verification result with details
|
|
889
|
+
*
|
|
890
|
+
* @example Verify winner proof (privacy-preserving)
|
|
891
|
+
* ```typescript
|
|
892
|
+
* const auction = new SealedBidAuction()
|
|
893
|
+
*
|
|
894
|
+
* // Observer only has: winner proof + original commitments (no amounts!)
|
|
895
|
+
* const commitments = [
|
|
896
|
+
* '0xabc...', // Unknown amount
|
|
897
|
+
* '0xdef...', // Unknown amount (this is the winner)
|
|
898
|
+
* '0x123...', // Unknown amount
|
|
899
|
+
* ]
|
|
900
|
+
*
|
|
901
|
+
* const proof = { ... } // Received winner proof
|
|
902
|
+
*
|
|
903
|
+
* // Verify without knowing losing bid amounts
|
|
904
|
+
* const verification = auction.verifyWinnerProof(proof, commitments)
|
|
905
|
+
* console.log(verification.valid) // true
|
|
906
|
+
* console.log(verification.details.bidsChecked) // 3
|
|
907
|
+
* ```
|
|
908
|
+
*
|
|
909
|
+
* @example Detect tampered proof
|
|
910
|
+
* ```typescript
|
|
911
|
+
* // Someone tries to modify commitments
|
|
912
|
+
* const tamperedCommitments = [
|
|
913
|
+
* '0xabc...',
|
|
914
|
+
* '0xFAKE...', // Changed!
|
|
915
|
+
* '0x123...',
|
|
916
|
+
* ]
|
|
917
|
+
*
|
|
918
|
+
* const verification = auction.verifyWinnerProof(proof, tamperedCommitments)
|
|
919
|
+
* console.log(verification.valid) // false
|
|
920
|
+
* console.log(verification.reason) // "commitments hash mismatch"
|
|
921
|
+
* ```
|
|
922
|
+
*/
|
|
923
|
+
verifyWinnerProof(proof: WinnerProof, allCommitments: HexString[]): WinnerVerification {
|
|
924
|
+
try {
|
|
925
|
+
// Validate inputs
|
|
926
|
+
if (!proof || !allCommitments || allCommitments.length === 0) {
|
|
927
|
+
return {
|
|
928
|
+
valid: false,
|
|
929
|
+
auctionId: proof?.auctionId || '',
|
|
930
|
+
winnerCommitment: proof?.winnerCommitment || ('0x' as HexString),
|
|
931
|
+
reason: 'missing required inputs',
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Check that total bids matches
|
|
936
|
+
if (proof.totalBids !== allCommitments.length) {
|
|
937
|
+
return {
|
|
938
|
+
valid: false,
|
|
939
|
+
auctionId: proof.auctionId,
|
|
940
|
+
winnerCommitment: proof.winnerCommitment,
|
|
941
|
+
reason: 'total bids mismatch',
|
|
942
|
+
details: {
|
|
943
|
+
bidsChecked: allCommitments.length,
|
|
944
|
+
comparisonsPassed: false,
|
|
945
|
+
hashMatched: false,
|
|
946
|
+
},
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Verify commitments hash
|
|
951
|
+
const sortedCommitments = [...allCommitments].sort()
|
|
952
|
+
const expectedHash = hash(sortedCommitments.join(','))
|
|
953
|
+
if (expectedHash !== proof.commitmentsHash) {
|
|
954
|
+
return {
|
|
955
|
+
valid: false,
|
|
956
|
+
auctionId: proof.auctionId,
|
|
957
|
+
winnerCommitment: proof.winnerCommitment,
|
|
958
|
+
reason: 'commitments hash mismatch - possible tampering',
|
|
959
|
+
details: {
|
|
960
|
+
bidsChecked: allCommitments.length,
|
|
961
|
+
comparisonsPassed: false,
|
|
962
|
+
hashMatched: false,
|
|
963
|
+
},
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Verify winner commitment is in the list
|
|
968
|
+
if (!allCommitments.includes(proof.winnerCommitment)) {
|
|
969
|
+
return {
|
|
970
|
+
valid: false,
|
|
971
|
+
auctionId: proof.auctionId,
|
|
972
|
+
winnerCommitment: proof.winnerCommitment,
|
|
973
|
+
reason: 'winner commitment not found in bid list',
|
|
974
|
+
details: {
|
|
975
|
+
bidsChecked: allCommitments.length,
|
|
976
|
+
comparisonsPassed: false,
|
|
977
|
+
hashMatched: true,
|
|
978
|
+
},
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Verify differential commitments count
|
|
983
|
+
const expectedDiffs = allCommitments.length - 1 // All bids except winner
|
|
984
|
+
if (proof.differentialCommitments.length !== expectedDiffs) {
|
|
985
|
+
return {
|
|
986
|
+
valid: false,
|
|
987
|
+
auctionId: proof.auctionId,
|
|
988
|
+
winnerCommitment: proof.winnerCommitment,
|
|
989
|
+
reason: 'incorrect number of differential commitments',
|
|
990
|
+
details: {
|
|
991
|
+
bidsChecked: allCommitments.length,
|
|
992
|
+
comparisonsPassed: false,
|
|
993
|
+
hashMatched: true,
|
|
994
|
+
},
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// All checks passed
|
|
999
|
+
return {
|
|
1000
|
+
valid: true,
|
|
1001
|
+
auctionId: proof.auctionId,
|
|
1002
|
+
winnerCommitment: proof.winnerCommitment,
|
|
1003
|
+
details: {
|
|
1004
|
+
bidsChecked: allCommitments.length,
|
|
1005
|
+
comparisonsPassed: true,
|
|
1006
|
+
hashMatched: true,
|
|
1007
|
+
},
|
|
1008
|
+
}
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return {
|
|
1011
|
+
valid: false,
|
|
1012
|
+
auctionId: proof?.auctionId || '',
|
|
1013
|
+
winnerCommitment: proof?.winnerCommitment || ('0x' as HexString),
|
|
1014
|
+
reason: `verification error: ${error instanceof Error ? error.message : 'unknown'}`,
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Create a new sealed-bid auction instance
|
|
1022
|
+
*
|
|
1023
|
+
* Convenience function for creating auction instances.
|
|
1024
|
+
*
|
|
1025
|
+
* @returns New SealedBidAuction instance
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* ```typescript
|
|
1029
|
+
* import { createSealedBidAuction } from '@sip-protocol/sdk'
|
|
1030
|
+
*
|
|
1031
|
+
* const auction = createSealedBidAuction()
|
|
1032
|
+
* const bid = auction.createBid({ auctionId: 'auction-1', amount: 100n })
|
|
1033
|
+
* ```
|
|
1034
|
+
*/
|
|
1035
|
+
export function createSealedBidAuction(): SealedBidAuction {
|
|
1036
|
+
return new SealedBidAuction()
|
|
1037
|
+
}
|