@zkthunder_/sdk 0.1.0 → 0.2.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 CHANGED
@@ -77,6 +77,23 @@ await new zk.Chain().verifyLineage(lin.proof, ctx); // LineageVe
77
77
  // record it: send zk.encodeSubmit(lin.proof, ctx) to zk.MAINNET.publicProofs
78
78
  ```
79
79
 
80
+ ## V2: full-coordinate verifiers on BN254
81
+
82
+ The same constructions on the BN254 curve, where the EVM has built-in point
83
+ addition and multiplication, so the V2 verifiers compute every scalar
84
+ multiplication themselves and compare whole points instead of 160 bit
85
+ addresses. Deployed on mainnet on 13 September 2026; the anchors and the gate
86
+ still use the V1 verifiers until the audit has compared the two, so use V2 for
87
+ your own verification and keep V1 for anything that feeds the live contracts.
88
+ V1 and V2 proofs are not interchangeable.
89
+
90
+ ```js
91
+ const rg = zk.proveRangeV2({ value: 1250n, threshold: 1000n, ctx: zk.context("my-app", "balance-check") });
92
+ await new zk.Chain().verifyRangeV2(rg.proof, rg.ctx); // true, checked on whole curve points
93
+ const lin = zk.proveLineageV2("previous state", "new state", ctx); // 17 words instead of 23
94
+ await new zk.Chain().verifyLineageV2(lin.proof, ctx);
95
+ ```
96
+
80
97
  ## API
81
98
 
82
99
  | Function | Purpose |
@@ -85,9 +102,10 @@ await new zk.Chain().verifyLineage(lin.proof, ctx); // LineageVe
85
102
  | `proveLineage(prev, out, ctx)` / `verifyLineage(proof, ctx)` | 23 word bundle for LineageVerifier |
86
103
  | `proveRange({ value, threshold, ctx, blinding? })` / `verifyRange(proof, ctx)` | range proof for RangeVerifier |
87
104
  | `proveTrustless(prev, out, ctx)` / `verifyTrustless(proof, ctx, prev, out)` | 37 word bound bundle for TrustlessAnchor |
105
+ | `commitV2`, `proveLineageV2` / `verifyLineageV2`, `proveRangeV2` / `verifyRangeV2`, `proveTrustlessV2` / `verifyTrustlessV2` | the same on BN254 for the V2 verifiers (17, 291 and 23 words) |
88
106
  | `context(...parts)`, `purposeId(name)`, `gateContext(gate, user, purpose)` | context words that bind proofs to their use |
89
107
  | `encodeVerify`, `encodeVerifyBound`, `encodeSubmit`, `encodeAttest`, `encodeGateProve`, `encodeIsEligible` | calldata for the deployed contracts |
90
- | `new Chain({ rpcUrl?, addresses? })` | `verifyLineage`, `verifyRange`, `verifyTrustless`, `isEligible`, `lineage`, `record`, `auditAnchor` |
108
+ | `new Chain({ rpcUrl?, addresses? })` | `verifyLineage`, `verifyRange`, `verifyTrustless`, `verifyLineageV2`, `verifyRangeV2`, `verifyTrustlessV2`, `isEligible`, `lineage`, `record`, `auditAnchor` |
91
109
  | `MAINNET` | deployed addresses on chain 4663 |
92
110
 
93
111
  Sending transactions is left to whatever wallet library you already use: the
@@ -97,8 +115,9 @@ encoders return calldata, and `MAINNET` has the addresses.
97
115
 
98
116
  - Range proofs cover a 32 bit difference between value and threshold and cost
99
117
  about 1.9 million gas to verify on chain. Zero is not a valid threshold.
100
- - The on-chain verifiers compare 160 bit point addresses (the ecrecover trick);
101
- a full coordinate check is on the roadmap.
118
+ - The V1 verifiers, which the live anchors and the gate use, compare 160 bit
119
+ point addresses (the ecrecover trick). The V2 verifiers check whole points on
120
+ BN254 and are deployed; the anchors switch to them after the audit.
102
121
  - These proofs establish commitment structure and lineage linkage. They do not
103
122
  prove a state transition was computed correctly; that is the zkVM tier.
104
123
 
package/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface Addresses {
5
5
  proofAnchor: string; lineageVerifier: string; publicProofs: string; rangeVerifier: string; boundVerifier: string; trustlessAnchor: string;
6
6
  /** empty until PrivateGate is deployed */
7
7
  privateGate: string;
8
+ /** V2 verifiers on BN254 (full-coordinate checks), deployed 2026-09-13; not yet in the anchoring path */
9
+ lineageVerifierV2: string; boundVerifierV2: string; rangeVerifierV2: string;
8
10
  }
9
11
  export const MAINNET: Readonly<Addresses>;
10
12
  export const NBITS: number;
@@ -17,6 +19,15 @@ export function verifyRange(proof: string | Uint8Array | bigint[], ctx: Bytes32L
17
19
  export function proveTrustless(prevRoot: Bytes32Like, outRoot: Bytes32Like, ctx: Bytes32Like): { proof: string; words: bigint[]; ctx: string; commitments: { in: Point; out: Point }; verified: boolean };
18
20
  export function verifyTrustless(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): boolean;
19
21
 
22
+ // V2: the same constructions on BN254 for the full-coordinate verifiers (17, 291 and 23 word bundles)
23
+ export function commitV2(value: Bytes32Like, blinding?: bigint | string | number): { commitment: Point; blinding: string };
24
+ export function proveLineageV2(prevRoot: Bytes32Like, outRoot: Bytes32Like, ctx: Bytes32Like): { proof: string; words: bigint[]; ctx: string; commitments: { in: Point; out: Point }; verified: boolean };
25
+ export function verifyLineageV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): boolean;
26
+ export function proveRangeV2(args: { value: bigint | number | string; threshold: bigint | number | string; ctx: Bytes32Like; blinding?: bigint | string }): { proof: string; words: bigint[]; ctx: string; commitment: Point; blinding: string; threshold: string; bits: number; verified: boolean };
27
+ export function verifyRangeV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): boolean;
28
+ export function proveTrustlessV2(prevRoot: Bytes32Like, outRoot: Bytes32Like, ctx: Bytes32Like): { proof: string; words: bigint[]; ctx: string; commitments: { in: Point; out: Point }; verified: boolean };
29
+ export function verifyTrustlessV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): boolean;
30
+
20
31
  export function context(...parts: Array<string | Uint8Array>): Uint8Array;
21
32
  export function purposeId(name: string): Uint8Array;
22
33
  export function gateContext(gate: string, user: string, purpose: string): Uint8Array;
@@ -41,6 +52,9 @@ export class Chain {
41
52
  verifyLineage(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
42
53
  verifyRange(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
43
54
  verifyTrustless(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): Promise<boolean>;
55
+ verifyLineageV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
56
+ verifyRangeV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
57
+ verifyTrustlessV2(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): Promise<boolean>;
44
58
  isEligible(gate: string, user: string, purpose: string, minThreshold: bigint | number | string, maxAge?: number): Promise<boolean>;
45
59
  lineage(): Promise<{ contract: string; lastSeq: number; lastEndBlock: number; lastRoot: string; latestBlock: number }>;
46
60
  record(seq: number): Promise<TrustlessRecord>;
package/index.js CHANGED
@@ -9,8 +9,11 @@ const S = require("./lib/sigma.js");
9
9
  const { proveBundle, verifyBundleJS } = require("./lib/prove.js");
10
10
  const R = require("./lib/range.js");
11
11
  const T = require("./lib/trustless.js");
12
+ const B2 = require("./lib/bn254.js");
13
+ const P2 = require("./lib/prove-v2.js");
14
+ const R2 = require("./lib/range-v2.js");
12
15
 
13
- /** Deployed addresses on Robinhood Chain mainnet (chain id 4663), all with verified source. */
16
+ /** Deployed addresses on Robinhood Chain mainnet (chain id 4663). V1 contracts have verified source; the V2 verifiers were deployed on 2026-09-13 and their verification is in progress. */
14
17
  const MAINNET = Object.freeze({
15
18
  chainId: 4663,
16
19
  rpcUrl: "https://rpc.mainnet.chain.robinhood.com",
@@ -23,6 +26,10 @@ const MAINNET = Object.freeze({
23
26
  trustlessAnchor: "0x6DF5BcE4799EdF35C98b5727aa25a7F2b67479a4",
24
27
  /** PrivateGate permissioning contract (deployed 2026-09-13); pass your own gate via new Chain({ addresses: { privateGate } }) */
25
28
  privateGate: "0xEc773A186532aC6080C258Edf883B726b13e2638",
29
+ /** V2 verifiers on BN254 (full-coordinate checks, EVM precompiles), deployed 2026-09-13. Not yet in the anchoring path: the anchors and the gate still use the verifiers above. */
30
+ lineageVerifierV2: "0xf11AB9C95eFDBDE842D9fe59008C1F74709f93e0",
31
+ boundVerifierV2: "0xA8De92742Bc88a746121866D0c2509027159ec54",
32
+ rangeVerifierV2: "0x24f35F760A499EEeF1E39B6FF8c34703102c6E21",
26
33
  });
27
34
 
28
35
  /* ------------------------------------------------------------ bytes */
@@ -104,6 +111,46 @@ function proveTrustless(prevRoot, outRoot, ctx) {
104
111
  /** Exact mirror of BoundLineageVerifier.verifyBound. */
105
112
  const verifyTrustless = (proof, ctx, prevRoot, outRoot) => T.verifyTrustlessJS(proofWords(proof), toBytes32(ctx), toBytes32(prevRoot), toBytes32(outRoot));
106
113
 
114
+ /* ------------------------------------------------------------ V2: BN254, full-coordinate verifiers */
115
+ /** Pedersen commitment on BN254, for the V2 verifiers. Same shape as commit(). */
116
+ function commitV2(value, blinding) {
117
+ const c = B2.commit(toBytes32(value), blinding === undefined ? undefined : big(blinding));
118
+ return { commitment: point(c.C), blinding: "0x" + c.r.toString(16).padStart(64, "0") };
119
+ }
120
+ /** V2 lineage bundle (17 words, 544 bytes) for LineageVerifierV2: no helper points, whole-point checks on chain. */
121
+ function proveLineageV2(prevRoot, outRoot, ctx) {
122
+ const c = toBytes32(ctx);
123
+ const b = P2.proveBundleV2(toBytes32(prevRoot), toBytes32(outRoot), c);
124
+ return { proof: b.hex, words: b.words, ctx: hex(c), commitments: { in: point(b.commitments.C_in), out: point(b.commitments.C_out) }, verified: P2.verifyBundleV2JS(b.words, c) };
125
+ }
126
+ /** Exact mirror of LineageVerifierV2.verify. */
127
+ const verifyLineageV2 = (proof, ctx) => P2.verifyBundleV2JS(proofWords(proof), toBytes32(ctx));
128
+
129
+ /** V2 range proof (291 words) for RangeVerifierV2. Same rules as proveRange; the commitment lives on BN254. */
130
+ function proveRangeV2({ value, threshold, ctx, blinding }) {
131
+ const v = big(value), x = big(threshold);
132
+ if (v < 0n || x < 0n) throw new Error("value and threshold must be non-negative");
133
+ if (x === 0n) throw new Error("threshold must be at least 1: every number is at least 0, so there is nothing to prove");
134
+ if (v < x) throw new Error("value is below the threshold: no valid proof exists");
135
+ if (v - x >= 1n << BigInt(R2.NBITS)) throw new Error(`value minus threshold must fit in ${R2.NBITS} bits`);
136
+ const r = blinding === undefined ? B2.randScalar() : big(blinding);
137
+ const C = B2.ecAdd(B2.ecMul(v, B2.G), B2.ecMul(r, B2.H));
138
+ const c = toBytes32(ctx);
139
+ const p = R2.proveRangeV2(C, v, r, x, c);
140
+ return { proof: hex(wordsToBytes(p.words)), words: p.words, ctx: hex(c), commitment: point(C), blinding: "0x" + r.toString(16).padStart(64, "0"), threshold: x.toString(), bits: R2.NBITS, verified: R2.verifyRangeV2JS(p.words, c) };
141
+ }
142
+ /** Exact mirror of RangeVerifierV2.verify. */
143
+ const verifyRangeV2 = (proof, ctx) => R2.verifyRangeV2JS(proofWords(proof), toBytes32(ctx));
144
+
145
+ /** V2 bound bundle (23 words) for BoundLineageVerifierV2: v*G is computed on chain from the caller's roots. */
146
+ function proveTrustlessV2(prevRoot, outRoot, ctx) {
147
+ const c = toBytes32(ctx), a = toBytes32(prevRoot), o = toBytes32(outRoot);
148
+ const t = P2.proveTrustlessV2(a, o, c);
149
+ return { proof: t.hex, words: t.words, ctx: hex(c), commitments: { in: point(t.commitments.C_in), out: point(t.commitments.C_out) }, verified: P2.verifyTrustlessV2JS(t.words, c, a, o) };
150
+ }
151
+ /** Exact mirror of BoundLineageVerifierV2.verifyBound. */
152
+ const verifyTrustlessV2 = (proof, ctx, prevRoot, outRoot) => P2.verifyTrustlessV2JS(proofWords(proof), toBytes32(ctx), toBytes32(prevRoot), toBytes32(outRoot));
153
+
107
154
  /* ------------------------------------------------------------ calldata */
108
155
  const sel = (sig) => S.hex(S.keccak256(S.utf8(sig))).slice(0, 8);
109
156
  const dyn = (u8) => { const padded = Math.ceil(u8.length / 32) * 32; const b = new Uint8Array(padded); b.set(u8); return pad32(u8.length) + S.hex(b); };
@@ -146,6 +193,12 @@ class Chain {
146
193
  verifyRange(proof, ctx) { return this.isTrue(this.addresses.rangeVerifier, encodeVerify(proof, ctx)); }
147
194
  /** Does the deployed BoundLineageVerifier accept this bound bundle for these public roots? */
148
195
  verifyTrustless(proof, ctx, prevRoot, outRoot) { return this.isTrue(this.addresses.boundVerifier, encodeVerifyBound(proof, ctx, prevRoot, outRoot)); }
196
+ /** V2: does LineageVerifierV2 (BN254, whole-point checks) accept this 17 word bundle? */
197
+ verifyLineageV2(proof, ctx) { return this.isTrue(this.addresses.lineageVerifierV2, encodeVerify(proof, ctx)); }
198
+ /** V2: does RangeVerifierV2 accept this 291 word range proof? */
199
+ verifyRangeV2(proof, ctx) { return this.isTrue(this.addresses.rangeVerifierV2, encodeVerify(proof, ctx)); }
200
+ /** V2: does BoundLineageVerifierV2 accept this 23 word bound bundle for these public roots? */
201
+ verifyTrustlessV2(proof, ctx, prevRoot, outRoot) { return this.isTrue(this.addresses.boundVerifierV2, encodeVerifyBound(proof, ctx, prevRoot, outRoot)); }
149
202
  /** Is `user` eligible on a PrivateGate? */
150
203
  isEligible(gate, user, purpose, minThreshold, maxAge = 0) { return this.isTrue(gate, encodeIsEligible(user, purpose, minThreshold, maxAge)); }
151
204
 
@@ -179,6 +232,7 @@ class Chain {
179
232
  module.exports = {
180
233
  MAINNET, Chain,
181
234
  commit, proveLineage, verifyLineage, proveRange, verifyRange, proveTrustless, verifyTrustless,
235
+ commitV2, proveLineageV2, verifyLineageV2, proveRangeV2, verifyRangeV2, proveTrustlessV2, verifyTrustlessV2,
182
236
  context, purposeId, gateContext, toBytes32,
183
237
  encodeVerify, encodeVerifyBound, encodeSubmit, encodeAttest, encodeGateProve, encodeIsEligible,
184
238
  NBITS: R.NBITS, keccak256: S.keccak256, hex, fromHex,
package/lib/bn254.js ADDED
@@ -0,0 +1,197 @@
1
+ // GENERATED COPY of zk/bn254.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder sigma layer, V2: the constructions of zk/sigma.js on the BN254
3
+ // curve (alt_bn128), where the EVM has precompiles for point addition (0x06)
4
+ // and scalar multiplication (0x07). A verifier on this curve computes every
5
+ // scalar multiplication itself and compares whole points, which removes the
6
+ // 160-bit address comparison the secp256k1 verifiers rely on (the ecrecover
7
+ // trick). Nothing here is deployed until the audit has looked at it.
8
+ //
9
+ // Zero dependencies. Mirrors sigma.js function for function: Pedersen
10
+ // commitments, Okamoto proofs of opening, Chaum-Pedersen equality, opening to
11
+ // a public value. Domain tags carry "bn254" so a proof can never be replayed
12
+ // across the two curves. Responses are canonical (below the group order) and
13
+ // the verifier rejects anything else, so every proof has one encoding.
14
+ //
15
+ // Scope, stated plainly: identical to V1. Privacy of the committed roots and
16
+ // integrity of the commitment structure, not transition correctness.
17
+ "use strict";
18
+ const S = require("./sigma.js");
19
+ const { keccak256, utf8, bigTo32, bytesToBig, hex } = S;
20
+
21
+ /* ---------------- BN254 (alt_bn128) G1: y^2 = x^3 + 3 over F_p ---------------- */
22
+ const P = 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
23
+ const N = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
24
+ const G = { x: 1n, y: 2n };
25
+ const O = null; // point at infinity; serialised as (0, 0), exactly as the precompiles do
26
+
27
+ const mod = (a, m) => ((a % m) + m) % m;
28
+ function powmod(b, e, m) { let r = 1n; b = mod(b, m); while (e > 0n) { if (e & 1n) r = (r * b) % m; b = (b * b) % m; e >>= 1n; } return r; }
29
+ const inv = (a, m) => powmod(mod(a, m), m - 2n, m);
30
+
31
+ function onCurve(Q) {
32
+ if (Q === O) return true;
33
+ if (Q.x < 0n || Q.x >= P || Q.y < 0n || Q.y >= P) return false;
34
+ return mod(Q.y * Q.y - (Q.x * Q.x * Q.x + 3n), P) === 0n;
35
+ }
36
+ /* the group has prime order N and cofactor 1, so every curve point is in the group */
37
+ function ecAdd(A, B) {
38
+ if (A === O) return B; if (B === O) return A;
39
+ if (A.x === B.x) {
40
+ if (mod(A.y + B.y, P) === 0n) return O;
41
+ const l = mod(3n * A.x * A.x * inv(2n * A.y, P), P);
42
+ const x = mod(l * l - 2n * A.x, P);
43
+ return { x, y: mod(l * (A.x - x) - A.y, P) };
44
+ }
45
+ const l = mod((B.y - A.y) * inv(B.x - A.x, P), P);
46
+ const x = mod(l * l - A.x - B.x, P);
47
+ return { x, y: mod(l * (A.x - x) - A.y, P) };
48
+ }
49
+ /* Jacobian doubling and addition for a = 0 curves (same formulas as sigma.js) */
50
+ function jDouble(X, Y, Z) {
51
+ if (Y === 0n) return [0n, 1n, 0n];
52
+ const A = mod(X * X, P), B = mod(Y * Y, P), C = mod(B * B, P);
53
+ const D = mod(2n * (mod((X + B) * (X + B), P) - A - C), P);
54
+ const E = mod(3n * A, P), F = mod(E * E, P);
55
+ const X3 = mod(F - 2n * D, P);
56
+ return [X3, mod(E * (D - X3) - 8n * C, P), mod(2n * Y * Z, P)];
57
+ }
58
+ function jAdd(X1, Y1, Z1, X2, Y2, Z2) {
59
+ if (Z1 === 0n) return [X2, Y2, Z2]; if (Z2 === 0n) return [X1, Y1, Z1];
60
+ const Z1Z1 = mod(Z1 * Z1, P), Z2Z2 = mod(Z2 * Z2, P);
61
+ const U1 = mod(X1 * Z2Z2, P), U2 = mod(X2 * Z1Z1, P);
62
+ const S1 = mod(Y1 * Z2 * Z2Z2, P), S2 = mod(Y2 * Z1 * Z1Z1, P);
63
+ const Hh = mod(U2 - U1, P), r = mod(S2 - S1, P);
64
+ if (Hh === 0n) { return r === 0n ? jDouble(X1, Y1, Z1) : [0n, 1n, 0n]; }
65
+ const HH = mod(Hh * Hh, P), HHH = mod(HH * Hh, P), V = mod(U1 * HH, P);
66
+ const X3 = mod(r * r - HHH - 2n * V, P);
67
+ return [X3, mod(r * (V - X3) - S1 * HHH, P), mod(Z1 * Z2 * Hh, P)];
68
+ }
69
+ function ecMul(k, Q) {
70
+ k = mod(k, N); if (Q === O || k === 0n) return O;
71
+ let R = [0n, 1n, 0n], T = [Q.x, Q.y, 1n];
72
+ while (k > 0n) { if (k & 1n) R = jAdd(R[0], R[1], R[2], T[0], T[1], T[2]); T = jDouble(T[0], T[1], T[2]); k >>= 1n; }
73
+ if (R[2] === 0n) return O;
74
+ const zi = inv(R[2], P), zi2 = mod(zi * zi, P);
75
+ return { x: mod(R[0] * zi2, P), y: mod(R[1] * zi2 * zi, P) };
76
+ }
77
+ function ecNeg(Q) { return Q === O ? O : { x: Q.x, y: Q.y === 0n ? 0n : P - Q.y }; }
78
+ const ecEq = (A, B) => (A === O || B === O) ? A === B : (A.x === B.x && A.y === B.y);
79
+ /* coordinates as the precompiles see them: infinity is (0, 0) */
80
+ const xy = Q => Q === O ? { x: 0n, y: 0n } : Q;
81
+
82
+ /* ---------------- H: second generator, nothing-up-my-sleeve ----------------
83
+ Try-and-increment from a fixed tag; discrete log of H wrt G unknown. */
84
+ function deriveH() {
85
+ if (P % 4n !== 3n) throw new Error("sqrt shortcut needs p = 3 mod 4");
86
+ for (let i = 0; ; i++) {
87
+ const x = mod(bytesToBig(keccak256(utf8("zkThunder/bn254/H/v1/" + i))), P);
88
+ const y2 = mod(x * x * x + 3n, P);
89
+ const y = powmod(y2, (P + 1n) / 4n, P);
90
+ if (mod(y * y, P) === y2) return { x, y: (y & 1n) ? P - y : y }; // even y
91
+ }
92
+ }
93
+ const H = deriveH();
94
+
95
+ /* ---------------- randomness (Node or browser) ---------------- */
96
+ function randomBytes32() {
97
+ const g = typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
98
+ if (g && typeof g.getRandomValues === "function") { const u = new Uint8Array(32); g.getRandomValues(u); return u; }
99
+ return new Uint8Array(require("crypto").randomBytes(32));
100
+ }
101
+ function randScalar() {
102
+ while (true) { const r = mod(bytesToBig(randomBytes32()), N); if (r > 0n) return r; }
103
+ }
104
+
105
+ /* ---------------- Fiat-Shamir challenge ----------------
106
+ keccak(tag | x1 | y1 | ... | extras) mod n, the byte layout the contract
107
+ reproduces with abi.encodePacked. Infinity is packed as (0, 0). */
108
+ function challenge(tag, points, extra32s) {
109
+ const parts = [utf8(tag)];
110
+ for (const Q of points) { const q = xy(Q); parts.push(bigTo32(q.x), bigTo32(q.y)); }
111
+ for (const e of (extra32s || [])) parts.push(e);
112
+ const total = parts.reduce((s, p2) => s + p2.length, 0);
113
+ const buf = new Uint8Array(total); let o = 0;
114
+ for (const p2 of parts) { buf.set(p2, o); o += p2.length; }
115
+ return mod(bytesToBig(keccak256(buf)), N);
116
+ }
117
+ const canonical = z => typeof z === "bigint" && z >= 0n && z < N;
118
+
119
+ /* ---------------- Pedersen commitment: C = x*G + r*H ---------------- */
120
+ function commit(value32, r) {
121
+ const x = mod(bytesToBig(value32), N);
122
+ r = r === undefined ? randScalar() : r;
123
+ return { C: ecAdd(ecMul(x, G), ecMul(r, H)), x, r };
124
+ }
125
+
126
+ /* ---------------- NIZK: proof of opening (Okamoto) ---------------- */
127
+ const TAG_OPEN = "zkThunder/bn254/open/v1";
128
+ function proveOpening(C, x, r, ctx32) {
129
+ const a = randScalar(), b = randScalar();
130
+ const A = ecAdd(ecMul(a, G), ecMul(b, H));
131
+ const c = challenge(TAG_OPEN, [C, A], [ctx32]);
132
+ return { A, z1: mod(a + c * x, N), z2: mod(b + c * r, N) };
133
+ }
134
+ function verifyOpening(C, prf, ctx32) {
135
+ if (C === O || prf.A === O || !onCurve(C) || !onCurve(prf.A)) return false;
136
+ if (!canonical(prf.z1) || !canonical(prf.z2)) return false;
137
+ const c = challenge(TAG_OPEN, [C, prf.A], [ctx32]);
138
+ const L = ecAdd(ecMul(prf.z1, G), ecMul(prf.z2, H));
139
+ const R = ecAdd(prf.A, ecMul(c, C));
140
+ return ecEq(L, R);
141
+ }
142
+
143
+ /* ---------------- NIZK: equality of committed values ----------------
144
+ D = C1 - C2 = (r1 - r2) H; Schnorr proof of knowledge of the dlog base H. */
145
+ const TAG_EQ = "zkThunder/bn254/eq/v1";
146
+ function proveEquality(C1, r1, C2, r2, ctx32) {
147
+ const rho = mod(r1 - r2, N);
148
+ const k = randScalar();
149
+ const A = ecMul(k, H);
150
+ const c = challenge(TAG_EQ, [C1, C2, A], [ctx32]);
151
+ return { A, z: mod(k + c * rho, N) };
152
+ }
153
+ function verifyEquality(C1, C2, prf, ctx32) {
154
+ if (C1 === O || C2 === O || prf.A === O || !onCurve(C1) || !onCurve(C2) || !onCurve(prf.A)) return false;
155
+ if (!canonical(prf.z)) return false;
156
+ const D = ecAdd(C1, ecNeg(C2));
157
+ if (D === O) return false; // identical commitments: reject
158
+ const c = challenge(TAG_EQ, [C1, C2, prf.A], [ctx32]);
159
+ const L = ecMul(prf.z, H);
160
+ const R = ecAdd(prf.A, ecMul(c, D));
161
+ return ecEq(L, R);
162
+ }
163
+
164
+ /* ---------------- NIZK: opening to a PUBLIC value ----------------
165
+ C = v*G + r*H for a public v: knowledge of r with C - v*G = r*H. The
166
+ verifier computes v*G itself; the prover supplies nothing about it. */
167
+ const TAG_PUB = "zkThunder/bn254/pub/v1";
168
+ function proveOpeningToPublic(C, v32, r, ctx32) {
169
+ const vG = ecMul(mod(bytesToBig(v32), N), G);
170
+ const k = randScalar();
171
+ const A = ecMul(k, H);
172
+ const c = challenge(TAG_PUB, [C, vG, A], [ctx32]);
173
+ return { A, z: mod(k + c * r, N) };
174
+ }
175
+ function verifyOpeningToPublic(C, v32, prf, ctx32) {
176
+ if (C === O || prf.A === O || !onCurve(C) || !onCurve(prf.A) || !canonical(prf.z)) return false;
177
+ const vG = ecMul(mod(bytesToBig(v32), N), G);
178
+ const D = ecAdd(C, ecNeg(vG)); if (D === O) return false;
179
+ const c = challenge(TAG_PUB, [C, vG, prf.A], [ctx32]);
180
+ const L = ecMul(prf.z, H);
181
+ const R = ecAdd(prf.A, ecMul(c, D));
182
+ return ecEq(L, R);
183
+ }
184
+
185
+ /* ---------------- serialization ---------------- */
186
+ function pt(Q) { const q = xy(Q); return { x: "0x" + q.x.toString(16).padStart(64, "0"), y: "0x" + q.y.toString(16).padStart(64, "0") }; }
187
+ function sc(s) { return "0x" + s.toString(16).padStart(64, "0"); }
188
+ function wordsToHex(words) { const buf = new Uint8Array(words.length * 32); words.forEach((v, i) => buf.set(bigTo32(BigInt(v)), i * 32)); return "0x" + hex(buf); }
189
+ function hexToWords(h) { const s = (h || "").replace(/^0x/, ""); if (s.length % 64) throw new Error("not a word array"); const out = []; for (let i = 0; i < s.length; i += 64) out.push(BigInt("0x" + s.slice(i, i + 64))); return out; }
190
+
191
+ module.exports = { keccak256, utf8, hex, bigTo32, bytesToBig,
192
+ P, N, G, H, O, mod, inv, powmod, onCurve, ecAdd, ecMul, ecNeg, ecEq, xy, canonical,
193
+ randScalar, challenge, commit,
194
+ proveOpening, verifyOpening, proveEquality, verifyEquality,
195
+ proveOpeningToPublic, verifyOpeningToPublic,
196
+ pt, sc, wordsToHex, hexToWords, deriveH,
197
+ TAG_OPEN, TAG_EQ, TAG_PUB };
@@ -0,0 +1,95 @@
1
+ // GENERATED COPY of zk/prove-v2.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder lineage bundles, V2 (BN254). Bare node, no deps.
3
+ //
4
+ // node zk/prove-v2.js <stateRootPrevHex32> <stateRootOutHex32> <ctxHex32>
5
+ //
6
+ // The V2 verifier computes every scalar multiplication itself through the
7
+ // EVM precompiles, so the bundle carries no helper points: 17 words instead
8
+ // of 23 for the lineage bundle, 23 instead of 37 for the bound bundle.
9
+ //
10
+ // Lineage bundle (LineageVerifierV2.verify), 17 words:
11
+ // [0..1] C_in [2..3] C_out [4..5] prevCout
12
+ // [6..7] openIn.A [8] z1 [9] z2
13
+ // [10..11] openOut.A [12] z1 [13] z2
14
+ // [14..15] eq.A [16] z
15
+ // Bound bundle (BoundLineageVerifierV2.verifyBound), 23 words:
16
+ // [0..16] lineage bundle
17
+ // [17..18] pubOut.A [19] z C_out opens to the header state root
18
+ // [20..21] pubIn.A [22] z C_in opens to the previous stored root
19
+ //
20
+ // verifyBundleV2JS / verifyTrustlessV2JS are exact mirrors of the Solidity
21
+ // checks (same challenges, same canonical-scalar rules, whole-point equality).
22
+ "use strict";
23
+ const B = require("./bn254.js");
24
+ const { mod, N, G, H, O, ecAdd, ecMul, ecNeg, ecEq, onCurve, canonical, commit, challenge,
25
+ randScalar, keccak256, utf8, hex, wordsToHex, bytesToBig } = B;
26
+
27
+ const ptAt = (w, i) => ({ x: w[i], y: w[i + 1] });
28
+ const validPt = Q => Q.x !== 0n || Q.y !== 0n ? onCurve(Q) : false; // (0,0) is infinity: rejected as input
29
+
30
+ /** Build the 17-word lineage bundle. Keeps the blindings so the bound bundle can extend it. */
31
+ function buildLineage(rootPrev32, rootOut32, ctx32) {
32
+ const prev = commit(rootPrev32);
33
+ const cin = commit(rootPrev32); // same value, fresh blinding
34
+ const cout = commit(rootOut32);
35
+ const oi = B.proveOpening(cin.C, cin.x, cin.r, ctx32);
36
+ const oo = B.proveOpening(cout.C, cout.x, cout.r, ctx32);
37
+ const eq = B.proveEquality(prev.C, prev.r, cin.C, cin.r, ctx32);
38
+ const words = [
39
+ cin.C.x, cin.C.y, cout.C.x, cout.C.y, prev.C.x, prev.C.y,
40
+ oi.A.x, oi.A.y, oi.z1, oi.z2,
41
+ oo.A.x, oo.A.y, oo.z1, oo.z2,
42
+ eq.A.x, eq.A.y, eq.z,
43
+ ];
44
+ return { words, prev, cin, cout };
45
+ }
46
+
47
+ function proveBundleV2(rootPrev32, rootOut32, ctx32) {
48
+ const b = buildLineage(rootPrev32, rootOut32, ctx32);
49
+ return { words: b.words, hex: wordsToHex(b.words),
50
+ commitments: { prevCout: b.prev.C, C_in: b.cin.C, C_out: b.cout.C } };
51
+ }
52
+
53
+ /** Exact mirror of LineageVerifierV2.verifyBundle. */
54
+ function verifyBundleV2JS(words, ctx32) {
55
+ if (words.length !== 17) return false;
56
+ const Cin = ptAt(words, 0), Cout = ptAt(words, 2), Prev = ptAt(words, 4);
57
+ if (!validPt(Cin) || !validPt(Cout) || !validPt(Prev)) return false;
58
+ if (!B.verifyOpening(Cin, { A: ptAt(words, 6), z1: words[8], z2: words[9] }, ctx32)) return false;
59
+ if (!B.verifyOpening(Cout, { A: ptAt(words, 10), z1: words[12], z2: words[13] }, ctx32)) return false;
60
+ return B.verifyEquality(Prev, Cin, { A: ptAt(words, 14), z: words[16] }, ctx32);
61
+ }
62
+
63
+ /** Bound bundle: lineage bundle plus openings of C_out and C_in to public roots. */
64
+ function proveTrustlessV2(rootPrev32, rootOut32, ctx32) {
65
+ const b = buildLineage(rootPrev32, rootOut32, ctx32);
66
+ const pubOut = B.proveOpeningToPublic(b.cout.C, rootOut32, b.cout.r, ctx32);
67
+ const pubIn = B.proveOpeningToPublic(b.cin.C, rootPrev32, b.cin.r, ctx32);
68
+ const words = [...b.words, pubOut.A.x, pubOut.A.y, pubOut.z, pubIn.A.x, pubIn.A.y, pubIn.z];
69
+ return { words, hex: wordsToHex(words),
70
+ commitments: { prevCout: b.prev.C, C_in: b.cin.C, C_out: b.cout.C } };
71
+ }
72
+
73
+ /** Exact mirror of BoundLineageVerifierV2.verifyBound. */
74
+ function verifyTrustlessV2JS(words, ctx32, rootPrev32, rootOut32) {
75
+ if (words.length !== 23) return false;
76
+ if (!verifyBundleV2JS(words.slice(0, 17), ctx32)) return false;
77
+ if (!B.verifyOpeningToPublic(ptAt(words, 2), rootOut32, { A: ptAt(words, 17), z: words[19] }, ctx32)) return false;
78
+ return B.verifyOpeningToPublic(ptAt(words, 0), rootPrev32, { A: ptAt(words, 20), z: words[22] }, ctx32);
79
+ }
80
+
81
+ module.exports = { proveBundleV2, verifyBundleV2JS, proveTrustlessV2, verifyTrustlessV2JS, buildLineage };
82
+
83
+ if (require.main === module) {
84
+ const [, , prevHex, outHex, ctxHex] = process.argv;
85
+ const h2b = h => Uint8Array.from((h || "").replace(/^0x/, "").padStart(64, "0").match(/../g).map(x => parseInt(x, 16)));
86
+ const rootPrev = prevHex ? h2b(prevHex) : keccak256(utf8("demo-root-prev"));
87
+ const rootOut = outHex ? h2b(outHex) : keccak256(utf8("demo-root-out"));
88
+ const ctx = ctxHex ? h2b(ctxHex) : keccak256(utf8("demo-ctx"));
89
+ const b = proveTrustlessV2(rootPrev, rootOut, ctx);
90
+ console.log(JSON.stringify({
91
+ ctx: "0x" + hex(ctx), rootPrev: "0x" + hex(rootPrev), rootOut: "0x" + hex(rootOut),
92
+ bundle: b.hex, words: b.words.length,
93
+ verifies_locally: verifyTrustlessV2JS(b.words, ctx, rootPrev, rootOut),
94
+ }, null, 2));
95
+ }
@@ -0,0 +1,82 @@
1
+ // GENERATED COPY of zk/range-v2.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder range proofs, V2 (BN254): prove a committed value satisfies
3
+ // v >= X without revealing v. Same construction as zk/range.js (bit
4
+ // decomposition of v - X over NBITS bits, one CDS OR-proof per bit, a
5
+ // homomorphic sum check), but the verifier multiplies points itself through
6
+ // the EVM precompiles, so no helper points travel with the proof and every
7
+ // equation is checked on whole points.
8
+ //
9
+ // Layout (all uint256 words):
10
+ // [0..1] C [2] threshold
11
+ // then per bit i (0..NBITS-1), 9 words:
12
+ // B.x B.y A0.x A0.y A1.x A1.y c0 z0 z1
13
+ // 3 + 9*32 = 291 words (483 in V1).
14
+ "use strict";
15
+ const B = require("./bn254.js");
16
+ const { mod, N, G, H, O, ecAdd, ecMul, ecNeg, ecEq, onCurve, canonical, challenge, randScalar, powmod } = B;
17
+
18
+ const NBITS = 32;
19
+ const TAG_BIT = "zkThunder/bn254/range/bit/v1";
20
+ const bitChallenge = (Bp, A0, A1, ctx32) => challenge(TAG_BIT, [Bp, A0, A1], [ctx32]);
21
+ const validPt = Q => (Q.x !== 0n || Q.y !== 0n) && onCurve(Q);
22
+
23
+ /** Prove v >= threshold for C = v*G + r*H. */
24
+ function proveRangeV2(C, v, r, threshold, ctx32, nbits = NBITS) {
25
+ const d = v - threshold;
26
+ if (d < 0n) throw new Error("value is below the threshold; cannot prove");
27
+ if (d >= (1n << BigInt(nbits))) throw new Error("difference exceeds range");
28
+ const blind = [];
29
+ let acc = 0n;
30
+ for (let i = 0; i < nbits - 1; i++) { blind.push(randScalar()); acc = mod(acc + (blind[i] << BigInt(i)), N); }
31
+ blind.push(mod((r - acc) * powmod(1n << BigInt(nbits - 1), N - 2n, N), N)); // sum(2^i r_i) == r
32
+
33
+ const words = [C.x, C.y, threshold];
34
+ for (let i = 0; i < nbits; i++) {
35
+ const b = (d >> BigInt(i)) & 1n;
36
+ const Bp = ecAdd(b ? G : O, ecMul(blind[i], H));
37
+ const BmG = ecAdd(Bp, ecNeg(G));
38
+ let A0, A1, c0, c1, z0, z1;
39
+ const k = randScalar();
40
+ if (b === 0n) { // real branch 0, simulated branch 1
41
+ c1 = randScalar(); z1 = randScalar();
42
+ A1 = ecAdd(ecMul(z1, H), ecNeg(ecMul(c1, BmG)));
43
+ A0 = ecMul(k, H);
44
+ const c = bitChallenge(Bp, A0, A1, ctx32);
45
+ c0 = mod(c - c1, N); z0 = mod(k + c0 * blind[i], N);
46
+ } else { // real branch 1, simulated branch 0
47
+ c0 = randScalar(); z0 = randScalar();
48
+ A0 = ecAdd(ecMul(z0, H), ecNeg(ecMul(c0, Bp)));
49
+ A1 = ecMul(k, H);
50
+ const c = bitChallenge(Bp, A0, A1, ctx32);
51
+ c1 = mod(c - c0, N); z1 = mod(k + c1 * blind[i], N);
52
+ }
53
+ words.push(Bp.x, Bp.y, A0.x, A0.y, A1.x, A1.y, c0, z0, z1);
54
+ }
55
+ return { words, nbits };
56
+ }
57
+
58
+ /** Exact mirror of RangeVerifierV2.verify (Horner sum, whole-point equality). */
59
+ function verifyRangeV2JS(words, ctx32, nbits = NBITS) {
60
+ if (words.length !== 3 + 9 * nbits) return false;
61
+ const pt = i => ({ x: words[i], y: words[i + 1] });
62
+ const C = pt(0), threshold = words[2];
63
+ if (!validPt(C)) return false;
64
+ let sum = O;
65
+ for (let j = 0; j < nbits; j++) {
66
+ const i = nbits - 1 - j, o = 3 + 9 * i;
67
+ const Bp = pt(o), A0 = pt(o + 2), A1 = pt(o + 4), c0 = words[o + 6], z0 = words[o + 7], z1 = words[o + 8];
68
+ if (![Bp, A0, A1].every(validPt)) return false;
69
+ if (![c0, z0, z1].every(canonical)) return false;
70
+ const c = bitChallenge(Bp, A0, A1, ctx32);
71
+ const c1 = mod(c - c0, N);
72
+ const BmG = ecAdd(Bp, ecNeg(G));
73
+ if (BmG === O) return false;
74
+ if (!ecEq(ecMul(z0, H), ecAdd(A0, ecMul(c0, Bp)))) return false; // z0 H == A0 + c0 B
75
+ if (!ecEq(ecMul(z1, H), ecAdd(A1, ecMul(c1, BmG)))) return false; // z1 H == A1 + c1 (B - G)
76
+ sum = j === 0 ? Bp : ecAdd(ecAdd(sum, sum), Bp); // Horner: sum = 2*sum + B_i
77
+ }
78
+ const Cp = ecAdd(C, ecNeg(ecMul(threshold, G))); // C - threshold*G
79
+ return ecEq(sum, Cp);
80
+ }
81
+
82
+ module.exports = { proveRangeV2, verifyRangeV2JS, NBITS, bitChallenge, TAG_BIT };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkthunder_/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Zero-knowledge proofs for Robinhood Chain: lineage bundles, range proofs and trustless anchors, generated locally with no dependencies, plus a tiny client for the deployed zkThunder contracts.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "test": "node test.js",
10
10
  "sync": "node sync.js"
11
11
  },
12
- "keywords": ["zero-knowledge", "pedersen", "range-proof", "robinhood-chain", "zkthunder", "secp256k1"],
12
+ "keywords": ["zero-knowledge", "pedersen", "range-proof", "robinhood-chain", "zkthunder", "secp256k1", "bn254"],
13
13
  "license": "Apache-2.0",
14
14
  "engines": { "node": ">=18" }
15
15
  }