@pinionengineering/prover-client 0.10.0 → 0.11.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @pinion/prover-client
1
+ # @pinionengineering/prover-client
2
2
 
3
3
  JavaScript / TypeScript client for **pinion-prover** storage-proof service.
4
4
 
@@ -9,22 +9,46 @@ Implements the SW-Pub challenger role:
9
9
  - Sends challenges to the prover and receives proofs
10
10
  - **Cryptographically verifies proofs** using BN254 Ate pairings
11
11
 
12
- Most IPFS pinning clients trust HTTP 200 as proof of storage but a server
12
+ Most IPFS pinning clients trust HTTP 200 as proof of storage, but a server
13
13
  can return 200 without actually holding your data. This library closes that gap:
14
14
  the server must solve a cryptographic challenge that is **only solvable by a party
15
15
  who has the block data**, and you verify the answer with a BN254 pairing equation
16
16
  in your browser or Node.js process.
17
17
 
18
+ Don't want to write JS? [The Go client in this same repo](../go#readme) documents a
19
+ no-code path: export a key + roots file from the
20
+ [dashboard](https://pinion.build), then run the same audit loop with the
21
+ `testclient` CLI. `testclient import` followed by `testclient audit --all --loop`
22
+ is the whole workflow, no account needed for the audit itself.
23
+
18
24
  ---
19
25
 
20
26
  ## Installation
21
27
 
22
28
  ```bash
23
- npm install @pinion/prover-client
29
+ npm install @pinionengineering/prover-client
24
30
  ```
25
31
 
26
32
  Requires Node.js ≥ 18. Works in modern browsers with native `crypto.getRandomValues` and `atob`.
27
33
 
34
+ ### Upgrading from 0.10.x
35
+
36
+ `tag()` and `prove()` are now **submit-only**: they return a job handle
37
+ immediately (`{ jobId, ... }`) instead of blocking until the job finishes.
38
+ Await the new `waitForTag(jobId, options?)` / `waitForProve(jobId, options?)`
39
+ to wait for a terminal state, or poll `tagStatus(jobId)`/`proveStatus(jobId)`
40
+ yourself. There is **no default deadline anymore**: the old hardcoded 60s
41
+ (`prove`)/10min (`tag`) timeouts are gone, since a proof or tag job can
42
+ legitimately take longer and the server was never designed to respect an
43
+ arbitrary client-side ceiling. If you want the old ceiling back, it's a
44
+ strict superset away: pass `signal: AbortSignal.timeout(ms)` to
45
+ `waitForProve()`/`waitForTag()`/`audit()`.
46
+
47
+ `audit(keyId, setup)` is unchanged in shape (still one call that submits,
48
+ waits, and verifies) but now also accepts `pollIntervalMs`/`signal`/
49
+ `onStatus` so you can build progress UI or cancel a long-running round; see
50
+ its section below.
51
+
28
52
  ---
29
53
 
30
54
  ## How It Works
@@ -35,14 +59,17 @@ The protocol has two distinct phases:
35
59
 
36
60
  1. Call `createKey()`. The server generates a BN254 key pair and stores the private
37
61
  scalar α. You receive:
38
- - `keyId` identifier for this key on the server
39
- - `publicKey` the public half: `V = α·G₂` (G2 point) and `U[0..s-1]` (G1 points).
62
+ - `keyId`: identifier for this key on the server
63
+ - `publicKey`: the public half: `V = α·G₂` (G2 point) and `U[0..s-1]` (G1 points).
40
64
  These are the values needed to verify proofs; store them if you want to verify
41
65
  independently of the server later.
42
66
 
43
- 2. Call `tag(cid, keyId)` for each pinned CID. The server walks the IPFS DAG,
44
- computes per-block authentication tags, and stores them. It returns the **block IDs**
45
- for that root — the `CID.Bytes()` of every block in the DAG, in TagList order.
67
+ 2. Call `tag(cid, keyId)` for each pinned CID, then `waitForTag(jobId)`.
68
+ Tagging is asynchronous. `tag()` submits the job and returns a job
69
+ handle immediately; the server walks the IPFS DAG, computes per-block
70
+ authentication tags, and stores them in the background. `waitForTag()`
71
+ polls until it's done and returns the **block IDs** for that root: the
72
+ `CID.Bytes()` of every block in the DAG, in TagList order.
46
73
 
47
74
  3. Call `getSetup(keyId)` to fetch the full setup document. The response contains:
48
75
  - The public key (same as returned by `createKey()`)
@@ -57,7 +84,7 @@ Each audit round is independent and stateless with respect to prior rounds:
57
84
 
58
85
  1. Generate a random 32-byte seed and select `n` blocks to challenge. Both client
59
86
  and server independently apply HMAC-SHA256 to the seed to rank all block IDs and
60
- select the same `n` blocks in the same order no extra communication needed.
87
+ select the same `n` blocks in the same order: no extra communication needed.
61
88
  Use `buildChallenge(n, total)` for an exact block count, or `audit()` for a
62
89
  percentage-based shorthand.
63
90
 
@@ -76,7 +103,7 @@ proof-of-storage evidence collected over time.
76
103
  ## Quick Start
77
104
 
78
105
  ```typescript
79
- import { PinionProverClient } from '@pinion/prover-client';
106
+ import { PinionProverClient } from '@pinionengineering/prover-client';
80
107
 
81
108
  const client = new PinionProverClient('https://example.com/prover', {
82
109
  getToken: async () => myAuthService.getToken(),
@@ -84,11 +111,13 @@ const client = new PinionProverClient('https://example.com/prover', {
84
111
 
85
112
  // ── Setup phase ────────────────────────────────────────────────────────────
86
113
 
87
- // Create a key returns the key ID and the public half of the key pair.
114
+ // Create a key: returns the key ID and the public half of the key pair.
88
115
  const { keyId, publicKey } = await client.createKey();
89
116
 
90
- // Tag a pinned CID the server walks the DAG and stores per-block auth tags.
91
- await client.tag('bafybeigdyrzt...', keyId);
117
+ // Tag a pinned CID: submits the job, then waits (no default deadline) for
118
+ // the server to walk the DAG and store per-block auth tags.
119
+ const tagJob = await client.tag('bafybeigdyrzt...', keyId);
120
+ await client.waitForTag(tagJob.jobId);
92
121
 
93
122
  // Fetch the setup document: public key + block ID lists for all tagged roots.
94
123
  const setup = await client.getSetup(keyId);
@@ -105,7 +134,7 @@ console.log(result.pass); // true = server cryptographically proved it holds you
105
134
  ## Examples
106
135
 
107
136
  `examples/verify.mjs` is a ready-to-run Node.js script covering the full
108
- flow find-or-create a key, tag new CIDs, run one audit round:
137
+ flow: find-or-create a key, tag new CIDs, run one audit round:
109
138
 
110
139
  ```sh
111
140
  PROVER_URL=https://hydrogen.pinion.build/prover \
@@ -159,13 +188,13 @@ The private scalar α never leaves the server. `publicKey` is the WireClientSet
159
188
  blob decoded from base64; you can store it and pass it directly to `verifyProof()`
160
189
  without ever calling `getSetup()` again.
161
190
 
162
- A single key covers many CIDs you do not need a new key per file.
191
+ A single key covers many CIDs: you do not need a new key per file.
163
192
 
164
193
  #### `client.listKeys()` → `ChallengeKeyInfo[]`
165
194
 
166
195
  Returns all keys for the authenticated user. Each entry includes:
167
- - `blocks_audited` cumulative blocks challenged across all audit rounds for this key
168
- - `audit_count` number of audit rounds completed
196
+ - `blocks_audited`: cumulative blocks challenged across all audit rounds for this key
197
+ - `audit_count`: number of audit rounds completed
169
198
 
170
199
  These are the long-running metrics that show how much proof-of-storage evidence
171
200
  has been accumulated over time.
@@ -178,47 +207,67 @@ Deletes a key and all associated tags.
178
207
 
179
208
  ### Setup Phase
180
209
 
181
- #### `client.tag(root, keyId)` → `TagResponse`
210
+ #### `client.tag(root, keyId)` → `TagSubmission`
182
211
 
183
212
  ```typescript
184
- const { block_ids, block_count } = await client.tag(cid, keyId);
213
+ const { jobId } = await client.tag(cid, keyId);
214
+ ```
215
+
216
+ Instructs the server to walk the IPFS DAG for `root`, compute per-block
217
+ authentication tags, and store them under `keyId`. The CID must be in the
218
+ `"pinned"` lifecycle state for the authenticated account. Submits the job
219
+ and returns immediately. This does not wait for tagging to finish.
220
+
221
+ #### `client.waitForTag(jobId, options?)` → `TagResponse`
222
+
223
+ ```typescript
224
+ const { block_ids, block_count } = await client.waitForTag(jobId, {
225
+ onProgress: (progress, status) => console.log(status, progress),
226
+ });
185
227
  // Exactly one is populated, depending on the key's protocol:
186
- // block_ids: string[] base64(CID.Bytes()) for every block, non-chunked
228
+ // block_ids: string[]: base64(CID.Bytes()) for every block, non-chunked
187
229
  // protocols only (Ateniese, Erway, BJO), in TagList order
188
- // block_count: number super-block count, chunked protocols only
189
- // (SW-Priv, SW-Pub) no per-block manifest is sent at all
230
+ // block_count: number : super-block count, chunked protocols only
231
+ // (SW-Priv, SW-Pub): no per-block manifest is sent at all
190
232
  ```
191
233
 
192
- Instructs the server to walk the IPFS DAG for `root`, compute per-block
193
- authentication tags, and store them under `keyId`. The CID must be in the
194
- `"pinned"` lifecycle state for the authenticated account.
234
+ Polls `tagStatus(jobId)` until the job reaches a terminal state. **No
235
+ default deadline**: waits as long as tagging takes unless `options.signal`
236
+ is given and fires first, in which case it throws `TagTimeoutError`. Throws
237
+ `TagFailedError` if the job reaches `"tag-failed"`.
195
238
 
196
239
  For non-chunked protocols, `block_ids` is the full list of blocks for this
197
240
  root in the order both client and server will use when ranking against a
198
- challenge seed. For chunked protocols, `block_count` is all the client needs
199
- challenge ids are synthesized locally via `superBlockId(rootBytes, i)` for
241
+ challenge seed. For chunked protocols, `block_count` is all the client needs:
242
+ challenge ids are synthesized locally via `superBlockId(rootBytes, i)` for
200
243
  `i` in `[0, block_count)`. Call `getSetup()` after tagging to get the
201
244
  combined list/count across all roots.
202
245
 
246
+ | `WaitForTagOptions` field | Type | Default | Description |
247
+ |-------|------|---------|-------------|
248
+ | `pollIntervalMs` | `number` | `1000` | Milliseconds between status polls. |
249
+ | `signal` | `AbortSignal` | none | Optional cancellation, no default deadline. |
250
+ | `onProgress` | `(progress, status) => void` | none | Called after every poll with `{total_blocks, completed_blocks}` and the raw status string. |
251
+
203
252
  #### `client.getSetup(keyId)` → `ParsedSetup`
204
253
 
205
254
  Fetches the complete setup document for a key.
206
255
 
207
256
  ```typescript
208
257
  const setup = await client.getSetup(keyId);
209
- // setup.clientSetup WireClientSetup public key material
258
+ // setup.clientSetup WireClientSetup: public key material
210
259
  // setup.roots [{ root: string, blockIds: Uint8Array[], chunked: boolean }, ...]
211
260
  // setup.totalBlocks total block count across all roots
212
261
  ```
213
262
 
214
263
  `blockIds` is always ready to pass to `buildChallenge()`/`verifyProof()`
215
264
  regardless of protocol. `chunked` tells you whether those ids are real
216
- per-block CIDs (`false`) or synthesized super-block ids (`true`) only
265
+ per-block CIDs (`false`) or synthesized super-block ids (`true`): only
217
266
  relevant if you need to re-derive or re-export ids yourself; synthesized ids
218
267
  are not valid CIDs and must never be round-tripped through `CID.decode()`.
219
268
 
220
269
  Call this once after tagging. Re-call whenever you add or remove roots.
221
- Pass the returned `ParsedSetup` directly to `audit()` it does not fetch
270
+ Pass the returned `ParsedSetup` directly to `audit()`: it does not fetch
222
271
  the setup for you.
223
272
 
224
273
  #### `client.deregister(keyId, root)`
@@ -268,13 +317,17 @@ const result = await client.audit(keyId, setup, {
268
317
  |-------|------|---------|-------------|
269
318
  | `roots` | `string[]` | all roots | Subset of registered CIDs to challenge. |
270
319
  | `challengePct` | `number` | `1` | Percentage of blocks to sample (0–100). |
320
+ | `pollIntervalMs` | `number` | `500` | Milliseconds between status polls during the wait phase. |
321
+ | `signal` | `AbortSignal` | none | Optional cancellation, no default deadline, waits as long as the proof takes unless this fires. Pass `AbortSignal.timeout(ms)` for a fixed ceiling. |
322
+ | `onStatus` | `(status: string) => void` | none | Called after every poll with the raw status string; proving has no per-block progress signal, unlike tagging. |
271
323
 
272
324
  Throws `PinNotActiveError` if any root is no longer in the `"pinned"` state.
273
- Throws `ProverError` on HTTP errors from the server.
325
+ Throws `ProverError` on HTTP errors from the server. Throws
326
+ `ProveTimeoutError` if `options.signal` fires before the proof completes.
274
327
 
275
328
  ---
276
329
 
277
- #### Low-level: `buildChallenge` + `prove` + `verifyProof`
330
+ #### Low-level: `buildChallenge` + `prove` + `waitForProve` + `verifyProof`
278
331
 
279
332
  Use these when you need an exact block count or want full control over the
280
333
  challenge parameters.
@@ -282,7 +335,7 @@ challenge parameters.
282
335
  ##### `buildChallenge(n, totalBlocks)` → `string`
283
336
 
284
337
  ```typescript
285
- import { buildChallenge } from '@pinion/prover-client';
338
+ import { buildChallenge } from '@pinionengineering/prover-client';
286
339
 
287
340
  const challenge = buildChallenge(10, setup.totalBlocks);
288
341
  // Returns base64(JSON({ suite_id: 1, seed: <32 random bytes>, c: 10, n: totalBlocks }))
@@ -292,18 +345,34 @@ Generates a random 32-byte seed and encodes a WireChallenge requesting `n` block
292
345
  out of `totalBlocks`. Both client and server independently apply HMAC-SHA256 to
293
346
  the seed to rank all block IDs and arrive at the same `n` blocks in the same order.
294
347
 
295
- ##### `client.prove(keyId, roots, challenge)` → `Uint8Array`
348
+ ##### `client.prove(keyId, roots, challenge)` → `ProveSubmission`
349
+
350
+ ```typescript
351
+ const { jobId } = await client.prove(keyId, roots, challenge);
352
+ ```
353
+
354
+ Posts the challenge to `POST /prove` and returns immediately with a job
355
+ handle. This does not wait for the proof to be computed.
356
+
357
+ ##### `client.waitForProve(jobId, options?)` → `Uint8Array`
296
358
 
297
359
  ```typescript
298
- const proofBytes = await client.prove(keyId, roots, challenge);
360
+ const proofBytes = await client.waitForProve(jobId, {
361
+ challenge, roots, // optional, but lets a thrown error carry enough to retry
362
+ signal: AbortSignal.timeout(60_000), // optional, no default deadline otherwise
363
+ });
299
364
  ```
300
365
 
301
- Posts the challenge to `POST /prove` and returns the raw proof bytes.
366
+ Polls `proveStatus(jobId)` until the job reaches a terminal state and
367
+ returns the raw proof bytes. **No default deadline**: waits as long as the
368
+ proof takes unless `options.signal` fires first, in which case it throws
369
+ `ProveTimeoutError`. Throws `ProveFailedError` if the job reaches
370
+ `"prove-failed"`.
302
371
 
303
372
  ##### `verifyProof(params)` → `boolean`
304
373
 
305
374
  ```typescript
306
- import { verifyProof } from '@pinion/prover-client';
375
+ import { verifyProof } from '@pinionengineering/prover-client';
307
376
 
308
377
  const pass = verifyProof({
309
378
  clientSetup: setup.clientSetup, // or publicKey from createKey()
@@ -314,26 +383,28 @@ const pass = verifyProof({
314
383
  ```
315
384
 
316
385
  Runs the BN254 pairing check locally. Returns `true` only if
317
- `e(σ,G₂) == e(A,V)`. Safe to call without a client instance useful for
386
+ `e(σ,G₂) == e(A,V)`. Safe to call without a client instance: useful for
318
387
  offline verification or integration with existing infrastructure.
319
388
 
320
389
  ##### Full low-level example
321
390
 
322
391
  ```typescript
323
- import { PinionProverClient, buildChallenge, verifyProof } from '@pinion/prover-client';
392
+ import { PinionProverClient, buildChallenge, verifyProof } from '@pinionengineering/prover-client';
324
393
 
325
394
  const client = new PinionProverClient(proverUrl, { getToken });
326
395
 
327
396
  // Setup phase
328
397
  const { keyId } = await client.createKey();
329
- await client.tag(cid, keyId);
398
+ const tagJob = await client.tag(cid, keyId);
399
+ await client.waitForTag(tagJob.jobId);
330
400
  const setup = await client.getSetup(keyId);
331
401
 
332
- // Audit phase exact block count
402
+ // Audit phase: exact block count
333
403
  const root = setup.roots[0]!;
334
404
  const blockIds = root.blockIds;
335
- const challenge = buildChallenge(10, blockIds.length);
336
- const proofBytes = await client.prove(keyId, [root.root], challenge);
405
+ const challenge = buildChallenge(10, blockIds.length);
406
+ const submission = await client.prove(keyId, [root.root], challenge);
407
+ const proofBytes = await client.waitForProve(submission.jobId, { challenge, roots: [root.root] });
337
408
 
338
409
  const pass = verifyProof({
339
410
  clientSetup: setup.clientSetup,
@@ -351,7 +422,7 @@ You can verify a proof with no HTTP client at all, as long as you have the
351
422
  public key and block IDs from a prior setup call:
352
423
 
353
424
  ```typescript
354
- import { buildChallenge, verifyProof, parseClientSetup } from '@pinion/prover-client';
425
+ import { buildChallenge, verifyProof, parseClientSetup } from '@pinionengineering/prover-client';
355
426
 
356
427
  const clientSetup = parseClientSetup(storedClientSetupBase64);
357
428
 
@@ -429,7 +500,7 @@ base64( JSON({ protocol, suite_id, s, l, name, v, u }) )
429
500
  | `suite_id` | `uint8` | `1` |
430
501
  | `s` | `int` | Number of sectors per block |
431
502
  | `l` | `int` | Challenge size parameter (from the SW-Pub scheme) |
432
- | `name` | `base64(16 bytes)` | File name λ random, unique per key, bound into every block tag |
503
+ | `name` | `base64(16 bytes)` | File name λ: random, unique per key, bound into every block tag |
433
504
  | `v` | `base64(128 bytes)` | Public key V = α·G₂ (G2 point, EIP-197 format) |
434
505
  | `u` | `base64(64 bytes)[]` | Public key U[0..s-1] (G1 points) |
435
506
 
@@ -460,13 +531,13 @@ e(σ, G₂) == e(Σₜ νₜ·H(λ‖idᵢₜ) + Σⱼ μⱼ·uⱼ, V)
460
531
 
461
532
  where:
462
533
 
463
- - `σ` proof accumulator G1 point (from server)
464
- - `G₂` G2 generator
465
- - `νₜ, iₜ` blinding coefficients and block indices (re-derived from seed)
466
- - `H(λ‖id)` RFC 9380 SVDW hash-to-G1 with DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"`
467
- - `μⱼ` per-sector Zr scalars (from server)
468
- - `uⱼ` public key G1 elements (from client_setup)
469
- - `V` public key `V = α·G₂` (from client_setup)
534
+ - `σ`: proof accumulator G1 point (from server)
535
+ - `G₂`: G2 generator
536
+ - `νₜ, iₜ`: blinding coefficients and block indices (re-derived from seed)
537
+ - `H(λ‖id)`: RFC 9380 SVDW hash-to-G1 with DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"`
538
+ - `μⱼ`: per-sector Zr scalars (from server)
539
+ - `uⱼ`: public key G1 elements (from client_setup)
540
+ - `V`: public key `V = α·G₂` (from client_setup)
470
541
 
471
542
  ### Hash-to-G1
472
543
 
@@ -481,7 +552,7 @@ points that are indistinguishable from uniform random G1 elements.
481
552
 
482
553
  The DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"` is fixed for all keys and matches
483
554
  the value in `storage-proofs/por/sw/pub.go`. **Changing this DST invalidates all
484
- existing tags** both sides derive H independently so they must agree.
555
+ existing tags**: both sides derive H independently so they must agree.
485
556
 
486
557
  ### Challenge Derivation (`DeriveChallenge`)
487
558
 
@@ -514,12 +585,18 @@ npm run test:gen
514
585
  npm test
515
586
  ```
516
587
 
517
- The test suite verifies:
588
+ `test/verify.test.mjs` verifies the cross-language crypto correctness:
518
589
  1. Valid proof accepted
519
590
  2. Tampered sigma rejected
520
591
  3. Wrong block IDs rejected
521
592
  4. Wrong public key rejected
522
593
 
594
+ `test/client.test.mjs` covers `PinionProverClient`'s submit/wait polling
595
+ model against a hand-rolled `fetch` stub: `prove()`/`tag()` submit without
596
+ polling, `waitForProve()`/`waitForTag()` poll to completion with no
597
+ implicit deadline, an `AbortSignal` cancels promptly, and `audit()`'s
598
+ `onStatus`/`pollIntervalMs` options reach the underlying poll.
599
+
523
600
  ---
524
601
 
525
602
  ## Implementation Notes