@pinionengineering/prover-client 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.
- package/README.md +534 -0
- package/dist/bn254.d.ts +59 -0
- package/dist/bn254.d.ts.map +1 -0
- package/dist/bn254.js +186 -0
- package/dist/bn254.js.map +1 -0
- package/dist/bn256.d.ts +68 -0
- package/dist/bn256.d.ts.map +1 -0
- package/dist/bn256.js +231 -0
- package/dist/bn256.js.map +1 -0
- package/dist/challenge.d.ts +59 -0
- package/dist/challenge.d.ts.map +1 -0
- package/dist/challenge.js +126 -0
- package/dist/challenge.js.map +1 -0
- package/dist/client.d.ts +132 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +231 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +67 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +94 -0
- package/dist/verify.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# @pinion/prover-client
|
|
2
|
+
|
|
3
|
+
JavaScript / TypeScript client for **pinion-prover** storage-proof service.
|
|
4
|
+
|
|
5
|
+
Implements the SW-Pub challenger role:
|
|
6
|
+
|
|
7
|
+
- Fetches the server's setup document (public key + block IDs)
|
|
8
|
+
- Constructs SW-Pub challenges client-side
|
|
9
|
+
- Sends challenges to the prover and receives proofs
|
|
10
|
+
- **Cryptographically verifies proofs** using BN254 Ate pairings
|
|
11
|
+
|
|
12
|
+
Most IPFS pinning clients trust HTTP 200 as proof of storage — but a server
|
|
13
|
+
can return 200 without actually holding your data. This library closes that gap:
|
|
14
|
+
the server must solve a cryptographic challenge that is **only solvable by a party
|
|
15
|
+
who has the block data**, and you verify the answer with a BN254 pairing equation
|
|
16
|
+
in your browser or Node.js process.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @pinion/prover-client
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Node.js ≥ 18. Works in modern browsers with native `crypto.getRandomValues` and `atob`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
The protocol has two distinct phases:
|
|
33
|
+
|
|
34
|
+
### Setup phase (done once per key)
|
|
35
|
+
|
|
36
|
+
1. Call `createKey()`. The server generates a BN254 key pair and stores the private
|
|
37
|
+
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).
|
|
40
|
+
These are the values needed to verify proofs; store them if you want to verify
|
|
41
|
+
independently of the server later.
|
|
42
|
+
|
|
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.
|
|
46
|
+
|
|
47
|
+
3. Call `getSetup(keyId)` to fetch the full setup document. The response contains:
|
|
48
|
+
- The public key (same as returned by `createKey()`)
|
|
49
|
+
- For each registered root: the complete ordered block ID list
|
|
50
|
+
|
|
51
|
+
Both the client and server know these block IDs, and they agree on their order.
|
|
52
|
+
The block ID list is what the challenge is drawn from.
|
|
53
|
+
|
|
54
|
+
### Audit phase (repeat on a schedule)
|
|
55
|
+
|
|
56
|
+
Each audit round is independent and stateless with respect to prior rounds:
|
|
57
|
+
|
|
58
|
+
1. Generate a random 32-byte seed and select `n` blocks to challenge. Both client
|
|
59
|
+
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.
|
|
61
|
+
Use `buildChallenge(n, total)` for an exact block count, or `audit()` for a
|
|
62
|
+
percentage-based shorthand.
|
|
63
|
+
|
|
64
|
+
2. Post the challenge `{ suite_id, seed, c, n }` to `POST /prove`.
|
|
65
|
+
|
|
66
|
+
3. Verify the proof locally using the public key. The BN254 pairing check
|
|
67
|
+
`e(σ, G₂) == e(A, V)` can only be satisfied by a party that holds the actual
|
|
68
|
+
block data.
|
|
69
|
+
|
|
70
|
+
Each passing audit round contributes to the cumulative `blocks_audited` counter
|
|
71
|
+
on the server (visible in `listKeys()`), giving a long-running record of
|
|
72
|
+
proof-of-storage evidence collected over time.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { PinionProverClient } from '@pinion/prover-client';
|
|
80
|
+
|
|
81
|
+
const client = new PinionProverClient('https://example.com/prover', {
|
|
82
|
+
getToken: async () => myAuthService.getToken(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── Setup phase ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
// Create a key — returns the key ID and the public half of the key pair.
|
|
88
|
+
const { keyId, publicKey } = await client.createKey();
|
|
89
|
+
|
|
90
|
+
// Tag a pinned CID — the server walks the DAG and stores per-block auth tags.
|
|
91
|
+
await client.tag('bafybeigdyrzt...', keyId);
|
|
92
|
+
|
|
93
|
+
// Fetch the setup document: public key + block ID lists for all tagged roots.
|
|
94
|
+
const setup = await client.getSetup(keyId);
|
|
95
|
+
|
|
96
|
+
// ── Audit phase (repeat on a schedule) ─────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
// Default: 1% spot-check per round.
|
|
99
|
+
const result = await client.audit(keyId, setup);
|
|
100
|
+
console.log(result.pass); // true = server cryptographically proved it holds your data
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
`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:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
PROVER_URL=https://hydrogen.pinion.build/prover \
|
|
112
|
+
PINION_TOKEN=eyJh... \
|
|
113
|
+
node examples/verify.mjs bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Expected output:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
[key] 9f3a1c2b-... (newly created)
|
|
120
|
+
[tag] registering 1 new CID(s)...
|
|
121
|
+
[setup] 64 total blocks across 1 root(s)
|
|
122
|
+
[audit] 1 block(s) challenged
|
|
123
|
+
|
|
124
|
+
[pass] Cryptographic proof: server holds the data.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## API Reference
|
|
130
|
+
|
|
131
|
+
### `new PinionProverClient(baseUrl, options?)`
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const client = new PinionProverClient(
|
|
135
|
+
'https://hydrogen.pinion.build/prover',
|
|
136
|
+
{ getToken: async () => 'Bearer ...' }
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`baseUrl` must include the path prefix (e.g., `/prover`). All authenticated endpoints use
|
|
141
|
+
`Bearer` tokens obtained from `getToken()`.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### Key Lifecycle
|
|
146
|
+
|
|
147
|
+
#### `client.createKey()` → `CreateKeyResult`
|
|
148
|
+
|
|
149
|
+
Creates a key pair on the server and returns the key ID and the **public key material**.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const { keyId, publicKey } = await client.createKey();
|
|
153
|
+
// publicKey.v G2 point V = α·G₂ (128 bytes, base64)
|
|
154
|
+
// publicKey.u G1 points U[0..s-1] (s × 64 bytes, base64)
|
|
155
|
+
// publicKey.name 16-byte file name λ bound into every block tag
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The private scalar α never leaves the server. `publicKey` is the WireClientSetup
|
|
159
|
+
blob decoded from base64; you can store it and pass it directly to `verifyProof()`
|
|
160
|
+
without ever calling `getSetup()` again.
|
|
161
|
+
|
|
162
|
+
A single key covers many CIDs — you do not need a new key per file.
|
|
163
|
+
|
|
164
|
+
#### `client.listKeys()` → `ChallengeKeyInfo[]`
|
|
165
|
+
|
|
166
|
+
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
|
|
169
|
+
|
|
170
|
+
These are the long-running metrics that show how much proof-of-storage evidence
|
|
171
|
+
has been accumulated over time.
|
|
172
|
+
|
|
173
|
+
#### `client.deleteKey(keyId)`
|
|
174
|
+
|
|
175
|
+
Deletes a key and all associated tags.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Setup Phase
|
|
180
|
+
|
|
181
|
+
#### `client.tag(root, keyId)` → `TagResponse`
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const { block_ids } = await client.tag(cid, keyId);
|
|
185
|
+
// block_ids: string[] — base64(CID.Bytes()) for every block, in TagList order
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Instructs the server to walk the IPFS DAG for `root`, compute per-block
|
|
189
|
+
authentication tags, and store them under `keyId`. The CID must be in the
|
|
190
|
+
`"pinned"` lifecycle state for the authenticated account.
|
|
191
|
+
|
|
192
|
+
`block_ids` is the full list of blocks for this root in the order both client
|
|
193
|
+
and server will use when ranking against a challenge seed. Call `getSetup()`
|
|
194
|
+
after tagging to get the combined list across all roots.
|
|
195
|
+
|
|
196
|
+
#### `client.getSetup(keyId)` → `ParsedSetup`
|
|
197
|
+
|
|
198
|
+
Fetches the complete setup document for a key.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const setup = await client.getSetup(keyId);
|
|
202
|
+
// setup.clientSetup WireClientSetup — public key material
|
|
203
|
+
// setup.roots [{ root: string, blockIds: Uint8Array[] }, ...]
|
|
204
|
+
// setup.totalBlocks total block count across all roots
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Call this once after tagging. Re-call whenever you add or remove roots.
|
|
208
|
+
Pass the returned `ParsedSetup` directly to `audit()` — it does not fetch
|
|
209
|
+
the setup for you.
|
|
210
|
+
|
|
211
|
+
#### `client.deregister(keyId, root)`
|
|
212
|
+
|
|
213
|
+
Removes the tag data for one root without deleting the key.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Audit Phase
|
|
218
|
+
|
|
219
|
+
#### `client.audit(keyId, setup, options?)` → `AuditResult`
|
|
220
|
+
|
|
221
|
+
Convenience wrapper for one full audit round:
|
|
222
|
+
|
|
223
|
+
1. Derives block count from `challengePct` and the setup's block list
|
|
224
|
+
2. Calls `buildChallenge(n, total)` to generate a random challenge
|
|
225
|
+
3. Posts it to `POST /prove` via `prove()`
|
|
226
|
+
4. Verifies the proof with `verifyProof()`
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Default: 1% of blocks (spot-check).
|
|
230
|
+
const result = await client.audit(keyId, setup);
|
|
231
|
+
|
|
232
|
+
// Full audit of all registered roots:
|
|
233
|
+
const result = await client.audit(keyId, setup, { challengePct: 100 });
|
|
234
|
+
|
|
235
|
+
// Audit a subset of roots:
|
|
236
|
+
const result = await client.audit(keyId, setup, {
|
|
237
|
+
roots: [specificCid],
|
|
238
|
+
challengePct: 50,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`AuditResult`:
|
|
243
|
+
```typescript
|
|
244
|
+
{
|
|
245
|
+
pass: boolean; // true = pairing equation satisfied
|
|
246
|
+
blocksChecked: number; // blocks sampled in this round
|
|
247
|
+
keyId: string;
|
|
248
|
+
roots: string[];
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`AuditOptions`:
|
|
253
|
+
|
|
254
|
+
| Field | Type | Default | Description |
|
|
255
|
+
|-------|------|---------|-------------|
|
|
256
|
+
| `roots` | `string[]` | all roots | Subset of registered CIDs to challenge. |
|
|
257
|
+
| `challengePct` | `number` | `1` | Percentage of blocks to sample (0–100). |
|
|
258
|
+
|
|
259
|
+
Throws `PinNotActiveError` if any root is no longer in the `"pinned"` state.
|
|
260
|
+
Throws `ProverError` on HTTP errors from the server.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
#### Low-level: `buildChallenge` + `prove` + `verifyProof`
|
|
265
|
+
|
|
266
|
+
Use these when you need an exact block count or want full control over the
|
|
267
|
+
challenge parameters.
|
|
268
|
+
|
|
269
|
+
##### `buildChallenge(n, totalBlocks)` → `string`
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { buildChallenge } from '@pinion/prover-client';
|
|
273
|
+
|
|
274
|
+
const challenge = buildChallenge(10, setup.totalBlocks);
|
|
275
|
+
// Returns base64(JSON({ suite_id: 1, seed: <32 random bytes>, c: 10, n: totalBlocks }))
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Generates a random 32-byte seed and encodes a WireChallenge requesting `n` blocks
|
|
279
|
+
out of `totalBlocks`. Both client and server independently apply HMAC-SHA256 to
|
|
280
|
+
the seed to rank all block IDs and arrive at the same `n` blocks in the same order.
|
|
281
|
+
|
|
282
|
+
##### `client.prove(keyId, roots, challenge)` → `Uint8Array`
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const proofBytes = await client.prove(keyId, roots, challenge);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Posts the challenge to `POST /prove` and returns the raw proof bytes.
|
|
289
|
+
|
|
290
|
+
##### `verifyProof(params)` → `boolean`
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { verifyProof } from '@pinion/prover-client';
|
|
294
|
+
|
|
295
|
+
const pass = verifyProof({
|
|
296
|
+
clientSetup: setup.clientSetup, // or publicKey from createKey()
|
|
297
|
+
blockIds, // Uint8Array[] from setup.roots[i].blockIds
|
|
298
|
+
challenge, // the string returned by buildChallenge()
|
|
299
|
+
proofBytes, // raw bytes from prove()
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Runs the BN254 pairing check locally. Returns `true` only if
|
|
304
|
+
`e(σ,G₂) == e(A,V)`. Safe to call without a client instance — useful for
|
|
305
|
+
offline verification or integration with existing infrastructure.
|
|
306
|
+
|
|
307
|
+
##### Full low-level example
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { PinionProverClient, buildChallenge, verifyProof } from '@pinion/prover-client';
|
|
311
|
+
|
|
312
|
+
const client = new PinionProverClient(proverUrl, { getToken });
|
|
313
|
+
|
|
314
|
+
// Setup phase
|
|
315
|
+
const { keyId } = await client.createKey();
|
|
316
|
+
await client.tag(cid, keyId);
|
|
317
|
+
const setup = await client.getSetup(keyId);
|
|
318
|
+
|
|
319
|
+
// Audit phase — exact block count
|
|
320
|
+
const root = setup.roots[0]!;
|
|
321
|
+
const blockIds = root.blockIds;
|
|
322
|
+
const challenge = buildChallenge(10, blockIds.length);
|
|
323
|
+
const proofBytes = await client.prove(keyId, [root.root], challenge);
|
|
324
|
+
|
|
325
|
+
const pass = verifyProof({
|
|
326
|
+
clientSetup: setup.clientSetup,
|
|
327
|
+
blockIds,
|
|
328
|
+
challenge,
|
|
329
|
+
proofBytes,
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
### Standalone Verification
|
|
336
|
+
|
|
337
|
+
You can verify a proof with no HTTP client at all, as long as you have the
|
|
338
|
+
public key and block IDs from a prior setup call:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { buildChallenge, verifyProof, parseClientSetup } from '@pinion/prover-client';
|
|
342
|
+
|
|
343
|
+
const clientSetup = parseClientSetup(storedClientSetupBase64);
|
|
344
|
+
|
|
345
|
+
// blockIds from a prior getSetup() call, decoded to Uint8Array[]:
|
|
346
|
+
const challenge = buildChallenge(5, blockIds.length);
|
|
347
|
+
|
|
348
|
+
// ...send challenge to POST /prove yourself, receive proofBytes...
|
|
349
|
+
|
|
350
|
+
const pass = verifyProof({ clientSetup, blockIds, challenge, proofBytes });
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Wire Format Specification
|
|
356
|
+
|
|
357
|
+
This section documents the exact encoding used by pinion-prover for clients
|
|
358
|
+
implementing the protocol in other languages.
|
|
359
|
+
|
|
360
|
+
### Curve
|
|
361
|
+
|
|
362
|
+
pinion-prover uses **BN254** (also known as alt\_bn128 or Ethereum's precompile curve,
|
|
363
|
+
standardised in [EIP-197](https://eips.ethereum.org/EIPS/eip-197)).
|
|
364
|
+
|
|
365
|
+
| Parameter | Value |
|
|
366
|
+
|-----------|-------|
|
|
367
|
+
| Curve | BN254 / alt\_bn128 (Ethereum EIP-197) |
|
|
368
|
+
| Field prime `p` | `0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47` |
|
|
369
|
+
| Group order `r` | `21888242871839275222246405745257275088548364400416034343698204186575808495617` |
|
|
370
|
+
| Curve equation | `y² = x³ + 3` over Fp |
|
|
371
|
+
| G1 generator | `(1, 2)` |
|
|
372
|
+
| G1 wire format | 64 bytes: `X_BE(32) ‖ Y_BE(32)` (no prefix byte) |
|
|
373
|
+
| G2 twist | D-type, `y² = x³ + 3/(9+i)` over Fp² |
|
|
374
|
+
| G2 wire format | 128 bytes: `X.im(32) ‖ X.re(32) ‖ Y.im(32) ‖ Y.re(32)` (EIP-197 / gnark-crypto `RawBytes`) |
|
|
375
|
+
| Hash-to-G1 | RFC 9380 SVDW, `XMD:SHA-256`, DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"` |
|
|
376
|
+
|
|
377
|
+
The G2 format matches gnark-crypto's `G2Affine.RawBytes()`. Each coordinate stores
|
|
378
|
+
the imaginary component first: `X.A1 ‖ X.A0 ‖ Y.A1 ‖ Y.A0` where `A0` is real
|
|
379
|
+
and `A1` is imaginary.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### Challenge (`WireChallenge`)
|
|
384
|
+
|
|
385
|
+
`POST /prove` body field `challenge`:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
base64( JSON({ suite_id, seed, c, n }) )
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
| Field | Type | Description |
|
|
392
|
+
|-------|------|-------------|
|
|
393
|
+
| `suite_id` | `uint8` | Always `1` (SuiteV1 = HMAC-SHA256) |
|
|
394
|
+
| `seed` | `base64(32 bytes)` | Cryptographically random seed |
|
|
395
|
+
| `c` | `int` | Number of blocks to challenge |
|
|
396
|
+
| `n` | `int` | Total blocks in the challenged store |
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
```json
|
|
400
|
+
{ "suite_id": 1, "seed": "A3Rk...==", "c": 5, "n": 100 }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
### Client Setup (`WireClientSetup`)
|
|
406
|
+
|
|
407
|
+
Returned by `createKey()` and embedded in the `GET /api/v1/setup` response:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
base64( JSON({ protocol, suite_id, s, l, name, v, u }) )
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
| Field | Type | Description |
|
|
414
|
+
|-------|------|-------------|
|
|
415
|
+
| `protocol` | `"swpub"` | Scheme identifier |
|
|
416
|
+
| `suite_id` | `uint8` | `1` |
|
|
417
|
+
| `s` | `int` | Number of sectors per block |
|
|
418
|
+
| `l` | `int` | Challenge size parameter (from the SW-Pub scheme) |
|
|
419
|
+
| `name` | `base64(16 bytes)` | File name λ — random, unique per key, bound into every block tag |
|
|
420
|
+
| `v` | `base64(128 bytes)` | Public key V = α·G₂ (G2 point, EIP-197 format) |
|
|
421
|
+
| `u` | `base64(64 bytes)[]` | Public key U[0..s-1] (G1 points) |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### Proof (`WireProof`)
|
|
426
|
+
|
|
427
|
+
`POST /prove` response body (raw JSON, despite `Content-Type: application/octet-stream`):
|
|
428
|
+
|
|
429
|
+
```json
|
|
430
|
+
{ "sigma": "<base64 64 bytes>", "mu": ["<base64 32 bytes>", ...] }
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
| Field | Type | Description |
|
|
434
|
+
|-------|------|-------------|
|
|
435
|
+
| `sigma` | `base64(64 bytes)` | Accumulated G1 point σ |
|
|
436
|
+
| `mu` | `base64(32 bytes)[]` | s × 32-byte Zr scalars μⱼ (big-endian) |
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Verification Equation
|
|
441
|
+
|
|
442
|
+
The SW-Pub scheme (Shacham & Waters, ASIACRYPT 2008 §3.3) uses the following pairing equation:
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
e(σ, G₂) == e(Σₜ νₜ·H(λ‖idᵢₜ) + Σⱼ μⱼ·uⱼ, V)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
where:
|
|
449
|
+
|
|
450
|
+
- `σ` — proof accumulator G1 point (from server)
|
|
451
|
+
- `G₂` — G2 generator
|
|
452
|
+
- `νₜ, iₜ` — blinding coefficients and block indices (re-derived from seed)
|
|
453
|
+
- `H(λ‖id)` — RFC 9380 SVDW hash-to-G1 with DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"`
|
|
454
|
+
- `μⱼ` — per-sector Zr scalars (from server)
|
|
455
|
+
- `uⱼ` — public key G1 elements (from client_setup)
|
|
456
|
+
- `V` — public key `V = α·G₂` (from client_setup)
|
|
457
|
+
|
|
458
|
+
### Hash-to-G1
|
|
459
|
+
|
|
460
|
+
`H(λ‖id)` uses the RFC 9380 straightline SVDW map-to-curve algorithm
|
|
461
|
+
([§6.6.2](https://www.rfc-editor.org/rfc/rfc9380.html#straightline-svdw))
|
|
462
|
+
rather than a scalar multiply `SHA-256(λ‖id) mod r · G₁`.
|
|
463
|
+
|
|
464
|
+
The scalar-multiply approach produces points with known discrete logarithms relative
|
|
465
|
+
to the generator, which breaks the scheme's security proof (§3.3 requires H to behave
|
|
466
|
+
as a random oracle over G1, not just a scalar multiple of a fixed base). SVDW produces
|
|
467
|
+
points that are indistinguishable from uniform random G1 elements.
|
|
468
|
+
|
|
469
|
+
The DST `"sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_"` is fixed for all keys and matches
|
|
470
|
+
the value in `storage-proofs/por/sw/pub.go`. **Changing this DST invalidates all
|
|
471
|
+
existing tags** — both sides derive H independently so they must agree.
|
|
472
|
+
|
|
473
|
+
### Challenge Derivation (`DeriveChallenge`)
|
|
474
|
+
|
|
475
|
+
Both client and server independently re-derive the same block selection from the
|
|
476
|
+
seed using SuiteV1 (HMAC-SHA256):
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
idxKey = HMAC-SHA256(seed, "indices")
|
|
480
|
+
coeffKey = HMAC-SHA256(seed, "coeffs")
|
|
481
|
+
rank[i] = HMAC-SHA256(idxKey, ids[i]) ← sort ascending, take first c positions
|
|
482
|
+
coeff[t] = HMAC-SHA256(coeffKey, BE64(t)) mod r
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`t` is encoded as **big-endian uint64** (8 bytes). The `c` selected block IDs are
|
|
486
|
+
the entries with the lowest-ranked HMAC values, in rank order. Because the ranking
|
|
487
|
+
is a deterministic function of the seed, client and server arrive at the same `c`
|
|
488
|
+
blocks in the same order without any additional communication.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Testing
|
|
493
|
+
|
|
494
|
+
Test vectors are generated by a Go program that runs the full Go server pipeline offline:
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
# Regenerate test vectors (requires Go)
|
|
498
|
+
npm run test:gen
|
|
499
|
+
|
|
500
|
+
# Run tests
|
|
501
|
+
npm test
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
The test suite verifies:
|
|
505
|
+
1. Valid proof accepted
|
|
506
|
+
2. Tampered sigma rejected
|
|
507
|
+
3. Wrong block IDs rejected
|
|
508
|
+
4. Wrong public key rejected
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Implementation Notes
|
|
513
|
+
|
|
514
|
+
### Curve operations
|
|
515
|
+
|
|
516
|
+
G1/G2 arithmetic and the Ate pairing delegate to
|
|
517
|
+
[@noble/curves](https://github.com/paulmillr/noble-curves) `bn254`, which implements
|
|
518
|
+
the same BN254 curve as gnark-crypto and Ethereum's precompile.
|
|
519
|
+
|
|
520
|
+
### Hash-to-G1 (SVDW)
|
|
521
|
+
|
|
522
|
+
[@noble/curves v1.9.7](https://github.com/paulmillr/noble-curves) has the SVDW
|
|
523
|
+
map-to-curve for BN254 G1 marked `notImplemented`. This library provides its own
|
|
524
|
+
implementation in `src/bn254.ts`, derived directly from gnark-crypto's
|
|
525
|
+
`MapToCurve1` in
|
|
526
|
+
[ecc/bn254/hash\_to\_g1.go](https://github.com/ConsenSys/gnark-crypto/blob/master/ecc/bn254/hash_to_g1.go).
|
|
527
|
+
|
|
528
|
+
`expand_message_xmd` (the hash expansion step) is provided by noble/curves and is not
|
|
529
|
+
reimplemented here. The custom code is limited to the 35-step straightline SVDW map
|
|
530
|
+
(RFC 9380 §6.6.2) plus the Fp arithmetic it requires.
|
|
531
|
+
|
|
532
|
+
Cross-language correctness is verified by the test suite: `npm run test:gen` runs the
|
|
533
|
+
full Go pipeline (gnark-crypto) and emits test vectors that `npm test` then validates
|
|
534
|
+
against the JS pairing check.
|
package/dist/bn254.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BN254 (alt_bn128 / Ethereum) curve operations using noble/curves.
|
|
3
|
+
*
|
|
4
|
+
* Curve parameters (Ethereum EIP-197 / gnark-crypto):
|
|
5
|
+
* p = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47
|
|
6
|
+
* q = 21888242871839275222246405745257275088548364400416034343698204186575808495617
|
|
7
|
+
* y² = x³ + 3 over Fp
|
|
8
|
+
*
|
|
9
|
+
* Wire formats (gnark-crypto RawBytes / EIP-197):
|
|
10
|
+
* G1: 64 bytes = X_BE(32) || Y_BE(32)
|
|
11
|
+
* G2: 128 bytes = X.A1(32) || X.A0(32) || Y.A1(32) || Y.A0(32)
|
|
12
|
+
* (gnark-crypto A0=real, A1=imaginary; noble c0=real, c1=imaginary)
|
|
13
|
+
*
|
|
14
|
+
* hashToG1 implements RFC 9380 hash-to-curve using the SVDW map with DST
|
|
15
|
+
* "sw-pub-v1-BN254G1_XMD:SHA-256_SVDW_RO_", matching gnark-crypto HashToG1
|
|
16
|
+
* in storage-proofs/por/sw/pub.go.
|
|
17
|
+
*
|
|
18
|
+
* noble/curves v1.9.7 has SVDW for BN254 G1 marked notImplemented, so the
|
|
19
|
+
* map is implemented here directly from RFC 9380 §6.6.2 using BigInt arithmetic.
|
|
20
|
+
* Constants are from gnark-crypto ecc/bn254/hash_to_g1.go, converted from
|
|
21
|
+
* Montgomery form.
|
|
22
|
+
*/
|
|
23
|
+
import { bn254 } from '@noble/curves/bn254';
|
|
24
|
+
export type G1Point = typeof bn254.G1.ProjectivePoint.BASE;
|
|
25
|
+
export type G2Point = typeof bn254.G2.ProjectivePoint.BASE;
|
|
26
|
+
export type Fp12Elem = ReturnType<typeof bn254.pairing>;
|
|
27
|
+
export declare const G1_BASE: G1Point;
|
|
28
|
+
export declare const G2_BASE: G2Point;
|
|
29
|
+
/** Convert big-endian bytes to a bigint. */
|
|
30
|
+
export declare function bytesToBigInt(bytes: Uint8Array): bigint;
|
|
31
|
+
/** Deserialize a G1 point from 64 bytes (gnark-crypto RawBytes / EIP-197 format). */
|
|
32
|
+
export declare function g1FromBytes(bytes: Uint8Array): G1Point;
|
|
33
|
+
/**
|
|
34
|
+
* Deserialize a G2 point from 128 bytes (gnark-crypto RawBytes / EIP-197 format).
|
|
35
|
+
*
|
|
36
|
+
* gnark-crypto G2Affine.RawBytes() layout:
|
|
37
|
+
* bytes[ 0: 32] = X.A1 (imaginary)
|
|
38
|
+
* bytes[32: 64] = X.A0 (real)
|
|
39
|
+
* bytes[64: 96] = Y.A1 (imaginary)
|
|
40
|
+
* bytes[96:128] = Y.A0 (real)
|
|
41
|
+
*
|
|
42
|
+
* noble/curves Fp2 uses { c0: real, c1: imaginary }.
|
|
43
|
+
*/
|
|
44
|
+
export declare function g2FromBytes(bytes: Uint8Array): G2Point;
|
|
45
|
+
/** Scalar-multiply a G1 point: returns k·P. */
|
|
46
|
+
export declare function g1ScalarMult(P: G1Point, k: bigint): G1Point;
|
|
47
|
+
/** Add two G1 points. */
|
|
48
|
+
export declare function g1Add(A: G1Point, B: G1Point): G1Point;
|
|
49
|
+
/** Compute the BN254 Ate pairing e(P, Q) → Fp12. */
|
|
50
|
+
export declare function atePairing(P: G1Point, Q: G2Point): Fp12Elem;
|
|
51
|
+
/** Check equality of two Fp12 elements. */
|
|
52
|
+
export declare function fp12Equal(a: Fp12Elem, b: Fp12Elem): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Hash bytes to a G1 point using RFC 9380 SVDW with the SW-Pub DST.
|
|
55
|
+
*
|
|
56
|
+
* Matches gnark-crypto HashToG1(msg, pubHashDST) in storage-proofs/por/sw/pub.go.
|
|
57
|
+
*/
|
|
58
|
+
export declare function hashToG1(msg: Uint8Array): G1Point;
|
|
59
|
+
//# sourceMappingURL=bn254.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bn254.d.ts","sourceRoot":"","sources":["../src/bn254.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAQ5C,MAAM,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC;AAC3D,MAAM,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC;AAC3D,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC;AAMxD,eAAO,MAAM,OAAO,EAAE,OAAuC,CAAC;AAC9D,eAAO,MAAM,OAAO,EAAE,OAAuC,CAAC;AAM9D,4CAA4C;AAC5C,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAIvD;AAMD,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAMtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAetD;AAMD,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,yBAAyB;AACzB,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAErD;AAMD,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,QAAQ,CAE3D;AAED,2CAA2C;AAC3C,wBAAgB,SAAS,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAE3D;AAkFD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAQjD"}
|