@zkthunder_/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @zkthunder_/sdk
2
+
3
+ Zero-knowledge proofs for Robinhood Chain, generated locally with no
4
+ dependencies, plus a tiny read-only client for the deployed zkThunder
5
+ contracts. Everything the zkthunder.net labs do, as library calls.
6
+
7
+ - Pedersen commitments on secp256k1: perfectly hiding, binding under discrete log.
8
+ - Lineage bundles: two hidden values, proofs they are well formed, and a proof they link.
9
+ - Range proofs: a hidden value is at least a public threshold (32 bit range).
10
+ - Bound bundles for the trustless lineage: commitments that provably open to public roots.
11
+ - Calldata encoders for every deployed contract, and a client that verifies on chain
12
+ over plain JSON-RPC.
13
+
14
+ Secrets never leave your process. The outputs are commitments, proofs and booleans.
15
+
16
+ ## Install
17
+
18
+ npm install @zkthunder_/sdk
19
+
20
+ Node 18 or newer (uses `fetch` and `BigInt`). No other dependencies.
21
+
22
+ ## Private eligibility with PrivateGate
23
+
24
+ A user proves that a hidden balance is at least a threshold, for a named
25
+ purpose, bound to the gate contract and to their own address. The gate stores
26
+ the threshold; any contract can then ask `isEligible` without the balance ever
27
+ appearing on chain.
28
+
29
+ ```js
30
+ const zk = require("@zkthunder_/sdk");
31
+
32
+ const GATE = "0x..."; // a deployed PrivateGate
33
+ const user = "0xYourAddress";
34
+ const ctx = zk.gateContext(GATE, user, "tier-1 access"); // keccak256(abi.encode(gate, user, purpose))
35
+
36
+ const { proof, commitment } = zk.proveRange({ value: 1250n, threshold: 1000n, ctx });
37
+ // send from `user`: to = GATE, data = zk.encodeGateProve("tier-1 access", proof)
38
+ // afterwards, from anywhere:
39
+ const chain = new zk.Chain();
40
+ await chain.isEligible(GATE, user, "tier-1 access", 1000n); // true, and the 1250 was never revealed
41
+ ```
42
+
43
+ The proof is useless anywhere else: another address, another gate or another
44
+ purpose changes the context, and the verifier folds the context into every
45
+ challenge.
46
+
47
+ ## Verify a range proof against the deployed verifier
48
+
49
+ ```js
50
+ const ctx = zk.context("my-app", "proof-of-reserves", "2026-09");
51
+ const rg = zk.proveRange({ value: 5_000_000n, threshold: 1_000_000n, ctx });
52
+ await new zk.Chain().verifyRange(rg.proof, ctx); // true: the RangeVerifier on mainnet accepts it
53
+ ```
54
+
55
+ To publish a public receipt from your own address, send
56
+ `zk.encodeAttest(rg.proof, ctx)` to `zk.MAINNET.rangeVerifier`; it emits
57
+ `RangeVerified(by, ctx, threshold, commitmentDigest)`.
58
+
59
+ ## Read and audit the trustless lineage
60
+
61
+ ```js
62
+ const chain = new zk.Chain();
63
+ const head = await chain.lineage(); // { lastSeq, lastEndBlock, lastRoot, latestBlock }
64
+ const audit = await chain.auditAnchor(); // re-reads the end block from the node and compares
65
+ console.log(audit.ok, audit.rootMatchesBlock, audit.linksToPrevious);
66
+ ```
67
+
68
+ Every root in the lineage was extracted by the contract from a block header it
69
+ checked against the chain's own block hash. No caller ever supplies a root.
70
+
71
+ ## Lineage bundles and the public registry
72
+
73
+ ```js
74
+ const ctx = zk.context("my-app/lineage", String(Date.now()));
75
+ const lin = zk.proveLineage("previous state", "new state", ctx);
76
+ await new zk.Chain().verifyLineage(lin.proof, ctx); // LineageVerifier says yes
77
+ // record it: send zk.encodeSubmit(lin.proof, ctx) to zk.MAINNET.publicProofs
78
+ ```
79
+
80
+ ## API
81
+
82
+ | Function | Purpose |
83
+ | --- | --- |
84
+ | `commit(value, blinding?)` | Pedersen commitment to a 32 byte value (text is hashed) |
85
+ | `proveLineage(prev, out, ctx)` / `verifyLineage(proof, ctx)` | 23 word bundle for LineageVerifier |
86
+ | `proveRange({ value, threshold, ctx, blinding? })` / `verifyRange(proof, ctx)` | range proof for RangeVerifier |
87
+ | `proveTrustless(prev, out, ctx)` / `verifyTrustless(proof, ctx, prev, out)` | 37 word bound bundle for TrustlessAnchor |
88
+ | `context(...parts)`, `purposeId(name)`, `gateContext(gate, user, purpose)` | context words that bind proofs to their use |
89
+ | `encodeVerify`, `encodeVerifyBound`, `encodeSubmit`, `encodeAttest`, `encodeGateProve`, `encodeIsEligible` | calldata for the deployed contracts |
90
+ | `new Chain({ rpcUrl?, addresses? })` | `verifyLineage`, `verifyRange`, `verifyTrustless`, `isEligible`, `lineage`, `record`, `auditAnchor` |
91
+ | `MAINNET` | deployed addresses on chain 4663 |
92
+
93
+ Sending transactions is left to whatever wallet library you already use: the
94
+ encoders return calldata, and `MAINNET` has the addresses.
95
+
96
+ ## Limits, stated plainly
97
+
98
+ - Range proofs cover a 32 bit difference between value and threshold and cost
99
+ 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.
102
+ - These proofs establish commitment structure and lineage linkage. They do not
103
+ prove a state transition was computed correctly; that is the zkVM tier.
104
+
105
+ ## Test
106
+
107
+ node test.js
108
+
109
+ Runs the local checks, then verifies fresh proofs against the mainnet contracts
110
+ when the RPC is reachable.
111
+
112
+ Apache-2.0.
package/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type Bytes32Like = string | Uint8Array;
2
+ export interface Point { x: string; y: string }
3
+ export interface Addresses {
4
+ chainId: number; rpcUrl: string; explorer: string;
5
+ proofAnchor: string; lineageVerifier: string; publicProofs: string; rangeVerifier: string; boundVerifier: string; trustlessAnchor: string;
6
+ /** empty until PrivateGate is deployed */
7
+ privateGate: string;
8
+ }
9
+ export const MAINNET: Readonly<Addresses>;
10
+ export const NBITS: number;
11
+
12
+ export function commit(value: Bytes32Like, blinding?: bigint | string | number): { commitment: Point; blinding: string };
13
+ export function proveLineage(prevRoot: Bytes32Like, outRoot: Bytes32Like, ctx: Bytes32Like): { proof: string; words: bigint[]; ctx: string; commitments: { in: Point; out: Point }; verified: boolean };
14
+ export function verifyLineage(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): boolean;
15
+ export function proveRange(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 };
16
+ export function verifyRange(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): boolean;
17
+ export function proveTrustless(prevRoot: Bytes32Like, outRoot: Bytes32Like, ctx: Bytes32Like): { proof: string; words: bigint[]; ctx: string; commitments: { in: Point; out: Point }; verified: boolean };
18
+ export function verifyTrustless(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): boolean;
19
+
20
+ export function context(...parts: Array<string | Uint8Array>): Uint8Array;
21
+ export function purposeId(name: string): Uint8Array;
22
+ export function gateContext(gate: string, user: string, purpose: string): Uint8Array;
23
+ export function toBytes32(v: Bytes32Like): Uint8Array;
24
+ export function keccak256(bytes: Uint8Array): Uint8Array;
25
+ export function hex(bytes: Uint8Array): string;
26
+ export function fromHex(hex: string): Uint8Array;
27
+
28
+ export function encodeVerify(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): string;
29
+ export function encodeVerifyBound(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): string;
30
+ export function encodeSubmit(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, tag?: Bytes32Like): string;
31
+ export function encodeAttest(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): string;
32
+ export function encodeGateProve(purpose: string, proof: string | Uint8Array | bigint[]): string;
33
+ export function encodeIsEligible(user: string, purpose: string, minThreshold: bigint | number | string, maxAge?: bigint | number): string;
34
+
35
+ export interface TrustlessRecord { seq: number; stateRootPrev: string; stateRootOut: string; cInDigest: string; cOutDigest: string; startBlock: number; endBlock: number; timestamp: number; prover: string }
36
+ export class Chain {
37
+ constructor(opts?: { rpcUrl?: string; addresses?: Partial<Addresses> });
38
+ rpcUrl: string; addresses: Addresses;
39
+ rpc(method: string, params: unknown[]): Promise<any>;
40
+ call(to: string, data: string): Promise<string>;
41
+ verifyLineage(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
42
+ verifyRange(proof: string | Uint8Array | bigint[], ctx: Bytes32Like): Promise<boolean>;
43
+ verifyTrustless(proof: string | Uint8Array | bigint[], ctx: Bytes32Like, prevRoot: Bytes32Like, outRoot: Bytes32Like): Promise<boolean>;
44
+ isEligible(gate: string, user: string, purpose: string, minThreshold: bigint | number | string, maxAge?: number): Promise<boolean>;
45
+ lineage(): Promise<{ contract: string; lastSeq: number; lastEndBlock: number; lastRoot: string; latestBlock: number }>;
46
+ record(seq: number): Promise<TrustlessRecord>;
47
+ auditAnchor(seq?: number): Promise<TrustlessRecord & { blockStateRoot: string; rootMatchesBlock: boolean; linksToPrevious: boolean | "genesis"; ok: boolean }>;
48
+ }
package/index.js ADDED
@@ -0,0 +1,185 @@
1
+ // zkThunder SDK: build and check zero-knowledge proofs for Robinhood Chain.
2
+ //
3
+ // No dependencies. Proofs are generated in this process; only commitments,
4
+ // proofs and verification results leave it. Chain access uses plain
5
+ // JSON-RPC over fetch (Node 18+), so no wallet library is required to read
6
+ // the lineage or to ask a verifier contract its opinion.
7
+ "use strict";
8
+ const S = require("./lib/sigma.js");
9
+ const { proveBundle, verifyBundleJS } = require("./lib/prove.js");
10
+ const R = require("./lib/range.js");
11
+ const T = require("./lib/trustless.js");
12
+
13
+ /** Deployed addresses on Robinhood Chain mainnet (chain id 4663), all with verified source. */
14
+ const MAINNET = Object.freeze({
15
+ chainId: 4663,
16
+ rpcUrl: "https://rpc.mainnet.chain.robinhood.com",
17
+ explorer: "https://robinhoodchain.blockscout.com",
18
+ proofAnchor: "0x54fE3b5A866190BF733104AB1c45d15405F27B16",
19
+ lineageVerifier: "0xDF3dD7eee2E2FD5e36900b882aa9473E72d56320",
20
+ publicProofs: "0x537819C42002691fD62e00bc251055587F51F502",
21
+ rangeVerifier: "0x003f80FF939dA662759ed7047D1a988e5a0E79A2",
22
+ boundVerifier: "0x9207ecf9ef5749ab39e17b610b1af974a03bd422",
23
+ trustlessAnchor: "0x6DF5BcE4799EdF35C98b5727aa25a7F2b67479a4",
24
+ /** PrivateGate permissioning contract (deployed 2026-09-13); pass your own gate via new Chain({ addresses: { privateGate } }) */
25
+ privateGate: "0xEc773A186532aC6080C258Edf883B726b13e2638",
26
+ });
27
+
28
+ /* ------------------------------------------------------------ bytes */
29
+ const hex = (u8) => "0x" + S.hex(u8);
30
+ const strip = (h) => (h.startsWith("0x") ? h.slice(2) : h);
31
+ const isHex = (s) => typeof s === "string" && /^0x[0-9a-fA-F]*$/.test(s) && s.length % 2 === 0;
32
+ const fromHex = (h) => Uint8Array.from((strip(h).match(/../g) || []).map((x) => parseInt(x, 16)));
33
+ const pad32 = (v) => BigInt(v).toString(16).padStart(64, "0");
34
+ const big = (v) => (typeof v === "bigint" ? v : BigInt(v));
35
+ const wordsToBytes = (words) => { const b = new Uint8Array(words.length * 32); words.forEach((w, i) => b.set(S.bigTo32(big(w)), i * 32)); return b; };
36
+ const bytesToWords = (u8) => { const out = []; for (let i = 0; i + 32 <= u8.length; i += 32) out.push(S.bytesToBig(u8.slice(i, i + 32))); return out; };
37
+ const point = (P) => ({ x: "0x" + P.x.toString(16).padStart(64, "0"), y: "0x" + P.y.toString(16).padStart(64, "0") });
38
+ const proofBytes = (proof) => (typeof proof === "string" ? fromHex(proof) : proof instanceof Uint8Array ? proof : wordsToBytes(proof));
39
+ const proofWords = (proof) => (Array.isArray(proof) ? proof.map(big) : bytesToWords(proofBytes(proof)));
40
+
41
+ /** Any value to 32 bytes: a 0x hex of 64 chars or a 32 byte array is used as-is; anything else is keccak256 of its UTF-8. */
42
+ function toBytes32(v) {
43
+ if (v instanceof Uint8Array && v.length === 32) return v;
44
+ if (typeof v === "string" && /^0x[0-9a-fA-F]{64}$/.test(v)) return fromHex(v);
45
+ return S.keccak256(S.utf8(String(v)));
46
+ }
47
+
48
+ /** A context word from any parts (strings, hex, bytes): keccak256 of their concatenation. Bind every proof to the place it is used. */
49
+ function context(...parts) {
50
+ const bufs = parts.map((p) => (p instanceof Uint8Array ? p : isHex(String(p)) ? fromHex(String(p)) : S.utf8(String(p))));
51
+ const all = new Uint8Array(bufs.reduce((n, b) => n + b.length, 0));
52
+ let o = 0; for (const b of bufs) { all.set(b, o); o += b.length; }
53
+ return S.keccak256(all);
54
+ }
55
+
56
+ /** keccak256("purpose name"): the bytes32 label PrivateGate and its consumers use. */
57
+ const purposeId = (name) => S.keccak256(S.utf8(name));
58
+
59
+ /** keccak256(abi.encode(gate, user, purpose)): exactly what PrivateGate.contextFor returns. `purpose` may be a name or a bytes32 hex. */
60
+ function gateContext(gate, user, purpose) {
61
+ const p = /^0x[0-9a-fA-F]{64}$/.test(String(purpose)) ? strip(purpose) : S.hex(purposeId(String(purpose)));
62
+ return S.keccak256(fromHex(pad32(gate) + pad32(user) + p));
63
+ }
64
+
65
+ /* ------------------------------------------------------------ proofs */
66
+
67
+ /** Pedersen commitment C = value*G + r*H to a 32 byte value (or a text, hashed). Perfectly hiding. */
68
+ function commit(value, blinding) {
69
+ const c = S.commit(toBytes32(value), blinding === undefined ? undefined : big(blinding));
70
+ return { commitment: point(c.C), blinding: "0x" + c.r.toString(16).padStart(64, "0") };
71
+ }
72
+
73
+ /** Lineage bundle (23 words, 736 bytes): commitments to two hidden values, openings for both, and an equality link. */
74
+ function proveLineage(prevRoot, outRoot, ctx) {
75
+ const c = toBytes32(ctx);
76
+ const b = proveBundle(toBytes32(prevRoot), toBytes32(outRoot), c);
77
+ return { proof: b.hex, words: b.words, ctx: hex(c), commitments: { in: point(b.commitments.C_in), out: point(b.commitments.C_out) }, verified: verifyBundleJS(b.words, c) };
78
+ }
79
+ /** Exact mirror of LineageVerifier.verify. */
80
+ const verifyLineage = (proof, ctx) => verifyBundleJS(proofWords(proof), toBytes32(ctx));
81
+
82
+ /** Range proof: a hidden `value` is at least the public `threshold`. The value never appears in the output. */
83
+ function proveRange({ value, threshold, ctx, blinding }) {
84
+ const v = big(value), x = big(threshold);
85
+ if (v < 0n || x < 0n) throw new Error("value and threshold must be non-negative");
86
+ if (x === 0n) throw new Error("threshold must be at least 1: every number is at least 0, so there is nothing to prove");
87
+ if (v < x) throw new Error("value is below the threshold: no valid proof exists");
88
+ if (v - x >= 1n << BigInt(R.NBITS)) throw new Error(`value minus threshold must fit in ${R.NBITS} bits`);
89
+ const r = blinding === undefined ? S.randScalar() : big(blinding);
90
+ const C = S.ecAdd(S.ecMul(v, S.G), S.ecMul(r, S.H));
91
+ const c = toBytes32(ctx);
92
+ const p = R.proveRange(C, v, r, x, c);
93
+ 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: R.NBITS, verified: R.verifyRangeJS(p.words, c) };
94
+ }
95
+ /** Exact mirror of RangeVerifier.verify. */
96
+ const verifyRange = (proof, ctx) => R.verifyRangeJS(proofWords(proof), toBytes32(ctx));
97
+
98
+ /** Bound bundle (37 words) for TrustlessAnchor: a lineage bundle whose commitments provably open to two public roots. */
99
+ function proveTrustless(prevRoot, outRoot, ctx) {
100
+ const c = toBytes32(ctx), a = toBytes32(prevRoot), b = toBytes32(outRoot);
101
+ const t = T.proveTrustlessFull(a, b, c);
102
+ return { proof: t.hex, words: t.words, ctx: hex(c), commitments: { in: point(t.commitments.C_in), out: point(t.commitments.C_out) }, verified: T.verifyTrustlessJS(t.words, c, a, b) };
103
+ }
104
+ /** Exact mirror of BoundLineageVerifier.verifyBound. */
105
+ const verifyTrustless = (proof, ctx, prevRoot, outRoot) => T.verifyTrustlessJS(proofWords(proof), toBytes32(ctx), toBytes32(prevRoot), toBytes32(outRoot));
106
+
107
+ /* ------------------------------------------------------------ calldata */
108
+ const sel = (sig) => S.hex(S.keccak256(S.utf8(sig))).slice(0, 8);
109
+ 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); };
110
+
111
+ /** verify(bytes proof, bytes32 ctx): LineageVerifier, RangeVerifier and BoundLineageVerifier all expose it (the bound verifier's has two extra roots). */
112
+ const encodeVerify = (proof, ctx) => "0x" + sel("verify(bytes,bytes32)") + pad32(0x40) + S.hex(toBytes32(ctx)) + dyn(proofBytes(proof));
113
+ /** verifyBound(bytes proof, bytes32 ctx, bytes32 rootPrev, bytes32 rootOut) on BoundLineageVerifier. */
114
+ const encodeVerifyBound = (proof, ctx, prevRoot, outRoot) => "0x" + sel("verifyBound(bytes,bytes32,bytes32,bytes32)") + pad32(0x80) + S.hex(toBytes32(ctx)) + S.hex(toBytes32(prevRoot)) + S.hex(toBytes32(outRoot)) + dyn(proofBytes(proof));
115
+ /** submit(bytes proof, bytes32 ctx, bytes32 tag) on PublicProofs: record a lineage bundle the verifier accepted. */
116
+ const encodeSubmit = (proof, ctx, tag = "0x" + "0".repeat(64)) => "0x" + sel("submit(bytes,bytes32,bytes32)") + pad32(0x60) + S.hex(toBytes32(ctx)) + S.hex(toBytes32(tag)) + dyn(proofBytes(proof));
117
+ /** attest(bytes proof, bytes32 ctx) on RangeVerifier: verify and emit a public RangeVerified receipt for the sender. */
118
+ const encodeAttest = (proof, ctx) => "0x" + sel("attest(bytes,bytes32)") + pad32(0x40) + S.hex(toBytes32(ctx)) + dyn(proofBytes(proof));
119
+ /** prove(bytes32 purpose, bytes proof) on PrivateGate. `purpose` may be a name or a bytes32 hex. */
120
+ const encodeGateProve = (purpose, proof) => { const p = /^0x[0-9a-fA-F]{64}$/.test(String(purpose)) ? strip(purpose) : S.hex(purposeId(String(purpose))); return "0x" + sel("prove(bytes32,bytes)") + p + pad32(0x40) + dyn(proofBytes(proof)); };
121
+ /** isEligible(address user, bytes32 purpose, uint256 minThreshold, uint256 maxAge) on PrivateGate. */
122
+ const encodeIsEligible = (user, purpose, minThreshold, maxAge = 0) => { const p = /^0x[0-9a-fA-F]{64}$/.test(String(purpose)) ? strip(purpose) : S.hex(purposeId(String(purpose))); return "0x" + sel("isEligible(address,bytes32,uint256,uint256)") + pad32(user) + p + pad32(minThreshold) + pad32(maxAge); };
123
+
124
+ /* ------------------------------------------------------------ chain */
125
+ const word = (data, i) => data.slice(2 + i * 64, 2 + (i + 1) * 64);
126
+ const num = (h) => Number(BigInt(h));
127
+
128
+ /** Read-only client for the deployed contracts. Pass { rpcUrl, addresses } to point elsewhere. */
129
+ class Chain {
130
+ constructor(opts = {}) {
131
+ this.rpcUrl = opts.rpcUrl || MAINNET.rpcUrl;
132
+ this.addresses = { ...MAINNET, ...(opts.addresses || {}) };
133
+ }
134
+ async rpc(method, params) {
135
+ const r = await fetch(this.rpcUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) });
136
+ const j = await r.json();
137
+ if (j.error) throw new Error(j.error.message || "rpc error");
138
+ return j.result;
139
+ }
140
+ call(to, data) { return this.rpc("eth_call", [{ to, data }, "latest"]); }
141
+ async isTrue(to, data) { return /1$/.test(await this.call(to, data)); }
142
+
143
+ /** Does the deployed LineageVerifier accept this bundle for this context? */
144
+ verifyLineage(proof, ctx) { return this.isTrue(this.addresses.lineageVerifier, encodeVerify(proof, ctx)); }
145
+ /** Does the deployed RangeVerifier accept this range proof for this context? */
146
+ verifyRange(proof, ctx) { return this.isTrue(this.addresses.rangeVerifier, encodeVerify(proof, ctx)); }
147
+ /** Does the deployed BoundLineageVerifier accept this bound bundle for these public roots? */
148
+ verifyTrustless(proof, ctx, prevRoot, outRoot) { return this.isTrue(this.addresses.boundVerifier, encodeVerifyBound(proof, ctx, prevRoot, outRoot)); }
149
+ /** Is `user` eligible on a PrivateGate? */
150
+ isEligible(gate, user, purpose, minThreshold, maxAge = 0) { return this.isTrue(gate, encodeIsEligible(user, purpose, minThreshold, maxAge)); }
151
+
152
+ /** Head of the trustless lineage. */
153
+ async lineage() {
154
+ const ta = this.addresses.trustlessAnchor;
155
+ const [seq, end, root, latest] = await Promise.all([
156
+ this.call(ta, "0x" + sel("lastSeq()")), this.call(ta, "0x" + sel("lastEndBlock()")), this.call(ta, "0x" + sel("lastRoot()")), this.rpc("eth_blockNumber", []),
157
+ ]);
158
+ return { contract: ta, lastSeq: num(seq), lastEndBlock: num(end), lastRoot: root, latestBlock: num(latest) };
159
+ }
160
+ /** One TrustlessAnchor record. */
161
+ async record(seq) {
162
+ const r = await this.call(this.addresses.trustlessAnchor, "0x" + sel("records(uint64)") + pad32(seq));
163
+ return { seq: Number(seq), stateRootPrev: "0x" + word(r, 0), stateRootOut: "0x" + word(r, 1), cInDigest: "0x" + word(r, 2), cOutDigest: "0x" + word(r, 3), startBlock: num("0x" + word(r, 4)), endBlock: num("0x" + word(r, 5)), timestamp: num("0x" + word(r, 6)), prover: "0x" + word(r, 7).slice(24) };
164
+ }
165
+ /** Independent audit of one record: the end block is re-read from the node and compared, and the link to the previous record is checked. */
166
+ async auditAnchor(seq) {
167
+ const head = await this.lineage();
168
+ const s = seq === undefined ? head.lastSeq : Number(seq);
169
+ if (s < 1 || s > head.lastSeq) throw new Error(`seq must be between 1 and ${head.lastSeq}`);
170
+ const rec = await this.record(s);
171
+ const blk = await this.rpc("eth_getBlockByNumber", ["0x" + rec.endBlock.toString(16), false]);
172
+ const rootMatchesBlock = String(blk.stateRoot).toLowerCase() === rec.stateRootOut.toLowerCase();
173
+ let linksToPrevious = "genesis";
174
+ if (s > 1) { const prev = await this.record(s - 1); linksToPrevious = prev.stateRootOut.toLowerCase() === rec.stateRootPrev.toLowerCase() && rec.startBlock === prev.endBlock + 1; }
175
+ return { ...rec, blockStateRoot: blk.stateRoot, rootMatchesBlock, linksToPrevious, ok: rootMatchesBlock && linksToPrevious !== false };
176
+ }
177
+ }
178
+
179
+ module.exports = {
180
+ MAINNET, Chain,
181
+ commit, proveLineage, verifyLineage, proveRange, verifyRange, proveTrustless, verifyTrustless,
182
+ context, purposeId, gateContext, toBytes32,
183
+ encodeVerify, encodeVerifyBound, encodeSubmit, encodeAttest, encodeGateProve, encodeIsEligible,
184
+ NBITS: R.NBITS, keccak256: S.keccak256, hex, fromHex,
185
+ };
package/lib/prove.js ADDED
@@ -0,0 +1,113 @@
1
+ // GENERATED COPY of zk/prove.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder sigma prover CLI + contract-logic mirror. Bare node, no deps.
3
+ //
4
+ // node zk/prove.js <stateRootPrevHex32> <stateRootOutHex32> <ctxHex32>
5
+ //
6
+ // Emits a 23-word proof bundle (hex) ready for LineageVerifier.verify,
7
+ // where publicInputsHash = ctx. Commits to:
8
+ // prevCout = Commit(rootPrev) (previous window's outgoing root)
9
+ // C_in = Commit(rootPrev) (this window's incoming root, fresh blinding)
10
+ // C_out = Commit(rootOut)
11
+ // and proves: opening of C_in, opening of C_out, equality(prevCout, C_in).
12
+ //
13
+ // Also exports verifyBundleJS(): an exact JS mirror of the Solidity
14
+ // verification path (same challenges, same helper points, same address
15
+ // checks) so the contract logic is testable without a chain.
16
+ "use strict";
17
+ const S = require("./sigma.js");
18
+ const { mod, N, P, G, H, ecAdd, ecMul, ecNeg, onCurve, pointAddr,
19
+ commit, keccak256, bigTo32, hex } = S;
20
+
21
+ const w32 = v => bigTo32(typeof v === "bigint" ? v : BigInt(v));
22
+
23
+ const TAG_OPEN_B = S.utf8(S.TAG_OPEN), TAG_EQ_B = S.utf8(S.TAG_EQ);
24
+ function packChallenge(tagBytes, points, ctx32){
25
+ const buf = new Uint8Array(tagBytes.length + 64*points.length + 32); let o = 0;
26
+ buf.set(tagBytes, o); o += tagBytes.length;
27
+ for (const Q of points){ buf.set(bigTo32(Q.x), o); buf.set(bigTo32(Q.y), o+32); o += 64; }
28
+ buf.set(ctx32, o);
29
+ return mod(S.bytesToBig(keccak256(buf)), N);
30
+ }
31
+ function challengeOpen(C, A, ctx32){ return packChallenge(TAG_OPEN_B, [C, A], ctx32); }
32
+ function challengeEq(C1, C2, A, ctx32){ return packChallenge(TAG_EQ_B, [C1, C2, A], ctx32); }
33
+
34
+ /** Build the 23-word bundle for LineageVerifier. */
35
+ function proveBundle(rootPrev32, rootOut32, ctx32){
36
+ const prev = commit(rootPrev32);
37
+ const cin = commit(rootPrev32); // same value, fresh blinding
38
+ const cout = commit(rootOut32);
39
+
40
+ // openings (contract-compatible challenges: on C, A, ctx)
41
+ function open(cm){
42
+ const a=S.randScalar(), b=S.randScalar();
43
+ const A=ecAdd(ecMul(a,G), ecMul(b,H));
44
+ const c=challengeOpen(cm.C, A, ctx32);
45
+ return { A, z1: mod(a+c*cm.x,N), z2: mod(b+c*cm.r,N),
46
+ CC: ecMul(c, cm.C) };
47
+ }
48
+ const oi = open(cin), oo = open(cout);
49
+
50
+ // equality(prevCout, C_in)
51
+ const rho = mod(prev.r - cin.r, N);
52
+ const k = S.randScalar();
53
+ const Aeq = ecMul(k, H);
54
+ const ceq = challengeEq(prev.C, cin.C, Aeq, ctx32);
55
+ const zeq = mod(k + ceq*rho, N);
56
+ const D = ecAdd(prev.C, ecNeg(cin.C));
57
+ const CD = ecMul(ceq, D);
58
+
59
+ const words = [
60
+ cin.C.x, cin.C.y, cout.C.x, cout.C.y, prev.C.x, prev.C.y,
61
+ oi.A.x, oi.A.y, oi.z1, oi.z2, oi.CC.x, oi.CC.y,
62
+ oo.A.x, oo.A.y, oo.z1, oo.z2, oo.CC.x, oo.CC.y,
63
+ Aeq.x, Aeq.y, zeq, CD.x, CD.y,
64
+ ];
65
+ const buf = new Uint8Array(23*32);
66
+ words.forEach((v,i)=>buf.set(w32(v), i*32));
67
+ return { hex: "0x"+hex(buf), words,
68
+ commitments: { prevCout: prev.C, C_in: cin.C, C_out: cout.C },
69
+ secrets: { note: "blindings held by prover; roots never revealed" } };
70
+ }
71
+
72
+ /** Exact JS mirror of LineageVerifier.verify (same checks, same order). */
73
+ function verifyBundleJS(words, ctx32){
74
+ const pt=(i)=>({x:words[i], y:words[i+1]});
75
+ const [Cin, Cout, Prev] = [pt(0), pt(2), pt(4)];
76
+ const onc=[0,2,4,6,10,12,16,18,21].every(i=>onCurve(pt(i)));
77
+ if(!onc) return false;
78
+
79
+ function vOpen(C, Ai, z1i, CCi){
80
+ const A=pt(Ai), z1=words[z1i], z2=words[z1i+1], CC=pt(CCi);
81
+ const c=challengeOpen(C, A, ctx32);
82
+ if(pointAddr(ecMul(c, C)) !== pointAddr(CC)) return false;
83
+ const R=ecAdd(A, CC);
84
+ return pointAddr(ecAdd(ecMul(z1,G), ecMul(z2,H))) === pointAddr(R);
85
+ }
86
+ if(!vOpen(Cin, 6, 8, 10)) return false;
87
+ if(!vOpen(Cout, 12, 14, 16)) return false;
88
+
89
+ const Aeq=pt(18), zeq=words[20], CD=pt(21);
90
+ const c=challengeEq(Prev, Cin, Aeq, ctx32);
91
+ const D=ecAdd(Prev, ecNeg(Cin));
92
+ if(D===null) return false;
93
+ if(pointAddr(ecMul(c,D)) !== pointAddr(CD)) return false;
94
+ const R=ecAdd(Aeq, CD);
95
+ return pointAddr(ecMul(zeq,H)) === pointAddr(R);
96
+ }
97
+
98
+ module.exports = { proveBundle, verifyBundleJS, challengeOpen, challengeEq };
99
+
100
+ if (require.main === module) {
101
+ const [,, prevHex, outHex, ctxHex] = process.argv;
102
+ const h2b = h => Uint8Array.from((h||"").replace(/^0x/,"").padStart(64,"0")
103
+ .match(/../g).map(x=>parseInt(x,16)));
104
+ const rootPrev = prevHex ? h2b(prevHex) : keccak256(S.utf8("demo-root-prev"));
105
+ const rootOut = outHex ? h2b(outHex) : keccak256(S.utf8("demo-root-out"));
106
+ const ctx = ctxHex ? h2b(ctxHex) : keccak256(S.utf8("demo-ctx"));
107
+ const b = proveBundle(rootPrev, rootOut, ctx);
108
+ console.log(JSON.stringify({
109
+ publicInputsHash: "0x"+hex(ctx),
110
+ proof: b.hex,
111
+ verifies_locally: verifyBundleJS(b.words, ctx),
112
+ }, null, 2));
113
+ }
package/lib/range.js ADDED
@@ -0,0 +1,121 @@
1
+ // GENERATED COPY of zk/range.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder range proofs: prove a committed value satisfies v >= X
3
+ // without revealing v. Zero dependencies; same primitives as sigma.js.
4
+ //
5
+ // Construction (bit decomposition, CDS OR-proofs):
6
+ // d = v - X must fit in NBITS bits. C' = C - X*G commits to d (same r).
7
+ // For each bit i: B_i = b_i*G + r_i*H with sum(2^i * r_i) = r, so that
8
+ // sum(2^i * B_i) == C' (holds iff the bits encode d and blindings add up).
9
+ // Each bit carries an OR-proof: "B_i = r_i*H" OR "B_i - G = r_i*H".
10
+ //
11
+ // On-chain friendliness: every scalar multiplication the verifier needs is
12
+ // supplied by the prover as a point and address-checked with the ecrecover
13
+ // trick (exactly like LineageVerifier), so verification needs no on-chain
14
+ // scalar multiplication, only affine additions.
15
+ "use strict";
16
+ const S = require("./sigma.js");
17
+ const { mod, N, G, H, ecAdd, ecMul, ecNeg, onCurve, pointAddr, keccak256, bigTo32, bytesToBig, utf8, randScalar } = S;
18
+
19
+ const NBITS = 32;
20
+ const TAG_BIT = utf8("zkThunder/range/bit/v1");
21
+
22
+ /* Fiat-Shamir challenge for one bit: keccak(tag | B | A0 | A1 | ctx) */
23
+ function bitChallenge(B, A0, A1, ctx32) {
24
+ const buf = new Uint8Array(TAG_BIT.length + 64 * 3 + 32); let o = 0;
25
+ buf.set(TAG_BIT, o); o += TAG_BIT.length;
26
+ for (const Q of [B, A0, A1]) { buf.set(bigTo32(Q.x), o); buf.set(bigTo32(Q.y), o + 32); o += 64; }
27
+ buf.set(ctx32, o);
28
+ return mod(bytesToBig(keccak256(buf)), N);
29
+ }
30
+
31
+ /**
32
+ * Prove v >= threshold for commitment C = v*G + r*H.
33
+ * Returns { words } laid out as (all uint256):
34
+ * [0..1] C [2] threshold
35
+ * then per bit i (0..NBITS-1), 13 words:
36
+ * B.x B.y A0.x A0.y A1.x A1.y c0 z0 z1 cB0.x cB0.y cB1.x cB1.y
37
+ * where cB0 = c0*B and cB1 = c1*(B - G) (helper points, address-checked)
38
+ * then per bit i, 2 words: P_i = 2^i * B_i (helper points for the sum)
39
+ * then 2 words: XG = threshold * G (helper point)
40
+ */
41
+ function proveRange(C, v, r, threshold, ctx32, nbits = NBITS) {
42
+ const d = v - threshold;
43
+ if (d < 0n) throw new Error("value is below the threshold; cannot prove");
44
+ if (d >= (1n << BigInt(nbits))) throw new Error("difference exceeds range");
45
+ const blind = [];
46
+ let acc = 0n;
47
+ for (let i = 0; i < nbits - 1; i++) { blind.push(randScalar()); acc = mod(acc + (blind[i] << BigInt(i)), N); }
48
+ // last blinding makes sum(2^i r_i) == r
49
+ const inv2 = powmodN(1n << BigInt(nbits - 1), N - 2n);
50
+ blind.push(mod((r - acc) * inv2, N));
51
+
52
+ const words = [C.x, C.y, threshold];
53
+ const bits = [], helpers = [];
54
+ for (let i = 0; i < nbits; i++) {
55
+ const b = (d >> BigInt(i)) & 1n;
56
+ const B = ecAdd(b ? G : null, ecMul(blind[i], H));
57
+ const BmG = ecAdd(B, ecNeg(G));
58
+ // CDS OR-proof: real branch for the true bit, simulated branch for the other
59
+ let A0, A1, c0, c1, z0, z1;
60
+ const k = randScalar();
61
+ if (b === 0n) {
62
+ // real: B = r H. simulate branch 1 (B - G = r H)
63
+ c1 = randScalar(); z1 = randScalar();
64
+ A1 = ecAdd(ecMul(z1, H), ecNeg(ecMul(c1, BmG)));
65
+ A0 = ecMul(k, H);
66
+ const c = bitChallenge(B, A0, A1, ctx32);
67
+ c0 = mod(c - c1, N); z0 = mod(k + c0 * blind[i], N);
68
+ } else {
69
+ c0 = randScalar(); z0 = randScalar();
70
+ A0 = ecAdd(ecMul(z0, H), ecNeg(ecMul(c0, B)));
71
+ A1 = ecMul(k, H);
72
+ const c = bitChallenge(B, A0, A1, ctx32);
73
+ c1 = mod(c - c0, N); z1 = mod(k + c1 * blind[i], N);
74
+ }
75
+ const cB0 = ecMul(c0, B), cB1 = ecMul(c1, BmG);
76
+ bits.push(B.x, B.y, A0.x, A0.y, A1.x, A1.y, c0, z0, z1, cB0.x, cB0.y, cB1.x, cB1.y);
77
+ const P = ecMul(1n << BigInt(i), B); helpers.push(P.x, P.y);
78
+ }
79
+ const XG = ecMul(threshold, G);
80
+ words.push(...bits, ...helpers, XG.x, XG.y);
81
+ return { words, nbits };
82
+ }
83
+
84
+ /** Exact mirror of the on-chain verification path. */
85
+ function verifyRangeJS(words, ctx32, nbits = NBITS) {
86
+ const pt = (i) => ({ x: words[i], y: words[i + 1] });
87
+ const C = pt(0), threshold = words[2];
88
+ if (!onCurve(C)) return false;
89
+ let o = 3;
90
+ let sum = null;
91
+ const helperBase = 3 + 13 * nbits;
92
+ for (let i = 0; i < nbits; i++, o += 13) {
93
+ const B = pt(o), A0 = pt(o + 2), A1 = pt(o + 4), c0 = words[o + 6], z0 = words[o + 7], z1 = words[o + 8], cB0 = pt(o + 9), cB1 = pt(o + 11);
94
+ if (![B, A0, A1, cB0, cB1].every(onCurve)) return false;
95
+ const c = bitChallenge(B, A0, A1, ctx32);
96
+ const c1 = mod(c - c0, N);
97
+ const BmG = ecAdd(B, ecNeg(G));
98
+ if (BmG === null) return false;
99
+ // helper points must be the claimed multiples (address check, as on chain)
100
+ if (pointAddr(ecMul(c0, B)) !== pointAddr(cB0)) return false;
101
+ if (pointAddr(ecMul(c1, BmG)) !== pointAddr(cB1)) return false;
102
+ // z0 H == A0 + c0 B and z1 H == A1 + c1 (B - G)
103
+ const R0 = ecAdd(A0, cB0), R1 = ecAdd(A1, cB1);
104
+ if (!R0 || !R1) return false;
105
+ if (pointAddr(ecMul(z0, H)) !== pointAddr(R0)) return false;
106
+ if (pointAddr(ecMul(z1, H)) !== pointAddr(R1)) return false;
107
+ // sum helper: P_i == 2^i * B_i
108
+ const P = pt(helperBase + 2 * i);
109
+ if (!onCurve(P) || pointAddr(ecMul(1n << BigInt(i), B)) !== pointAddr(P)) return false;
110
+ sum = ecAdd(sum, P);
111
+ }
112
+ const XG = pt(helperBase + 2 * nbits);
113
+ if (!onCurve(XG) || pointAddr(ecMul(threshold, G)) !== pointAddr(XG)) return false;
114
+ const Cprime = ecAdd(C, ecNeg(XG));
115
+ if (!sum || !Cprime) return false;
116
+ return sum.x === Cprime.x && sum.y === Cprime.y;
117
+ }
118
+
119
+ function powmodN(b, e) { let r = 1n; b = mod(b, N); while (e > 0n) { if (e & 1n) r = (r * b) % N; b = (b * b) % N; e >>= 1n; } return r; }
120
+
121
+ module.exports = { proveRange, verifyRangeJS, NBITS, bitChallenge };
package/lib/sigma.js ADDED
@@ -0,0 +1,244 @@
1
+ // GENERATED COPY of zk/sigma.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // zkThunder sigma layer — zero-dependency zero-knowledge proofs.
3
+ //
4
+ // Primitives (all from scratch, bare Node, no packages):
5
+ // - secp256k1 group arithmetic (BigInt)
6
+ // - Pedersen commitments C = x*G + r*H (perfectly hiding,
7
+ // computationally binding; H derived nothing-up-my-sleeve)
8
+ // - NIZK proof of opening (Okamoto / Schnorr on two generators,
9
+ // Fiat-Shamir with keccak256, domain-separated)
10
+ // - NIZK equality proof (Chaum-Pedersen variant): two commitments
11
+ // hide the SAME value, without revealing it
12
+ //
13
+ // zkThunder use: state roots become hiding commitments; each anchor
14
+ // proves its commitments are well-formed, and consecutive windows'
15
+ // boundary commitments are linked in zero knowledge.
16
+ //
17
+ // Scope, stated plainly: these proofs give privacy of the committed
18
+ // roots and integrity of the commitment structure. They do NOT prove
19
+ // the state transition itself is correct — that is the zkVM tier.
20
+
21
+ "use strict";
22
+
23
+ /* ---------------- keccak-256 (same impl as kats.js) ---------------- */
24
+ function keccak256(bytes){
25
+ const RC=[0x0000000000000001n,0x0000000000008082n,0x800000000000808an,0x8000000080008000n,
26
+ 0x000000000000808bn,0x0000000080000001n,0x8000000080008081n,0x8000000000008009n,
27
+ 0x000000000000008an,0x0000000000000088n,0x0000000080008009n,0x000000008000000an,
28
+ 0x000000008000808bn,0x800000000000008bn,0x8000000000008089n,0x8000000000008003n,
29
+ 0x8000000000008002n,0x8000000000000080n,0x000000000000800an,0x800000008000000an,
30
+ 0x8000000080008081n,0x8000000000008080n,0x0000000080000001n,0x8000000080008008n];
31
+ const ROT=[[0,36,3,41,18],[1,44,10,45,2],[62,6,43,15,61],[28,55,25,21,56],[27,20,39,8,14]];
32
+ const M=(1n<<64n)-1n; const rotl=(x,nn)=>((x<<BigInt(nn))|(x>>BigInt(64-nn)))&M;
33
+ const rate=136;
34
+ const padded=new Uint8Array(Math.ceil((bytes.length+1)/rate)*rate);
35
+ padded.set(bytes); padded[bytes.length]=0x01; padded[padded.length-1]|=0x80;
36
+ const S=Array.from({length:5},()=>new Array(5).fill(0n));
37
+ for(let off=0;off<padded.length;off+=rate){
38
+ for(let i=0;i<rate/8;i++){let lane=0n;
39
+ for(let b=7;b>=0;b--)lane=(lane<<8n)|BigInt(padded[off+i*8+b]);
40
+ S[i%5][(i/5)|0]^=lane;}
41
+ for(let r=0;r<24;r++){
42
+ const C=[],D=[];
43
+ for(let x=0;x<5;x++)C[x]=S[x][0]^S[x][1]^S[x][2]^S[x][3]^S[x][4];
44
+ for(let x=0;x<5;x++){D[x]=C[(x+4)%5]^rotl(C[(x+1)%5],1);
45
+ for(let y=0;y<5;y++)S[x][y]^=D[x];}
46
+ const B=Array.from({length:5},()=>new Array(5).fill(0n));
47
+ for(let x=0;x<5;x++)for(let y=0;y<5;y++)B[y][(2*x+3*y)%5]=rotl(S[x][y],ROT[x][y]);
48
+ for(let x=0;x<5;x++)for(let y=0;y<5;y++)S[x][y]=B[x][y]^(~B[(x+1)%5][y]&B[(x+2)%5][y]);
49
+ S[0][0]^=RC[r];
50
+ }
51
+ }
52
+ const out=new Uint8Array(32);
53
+ for(let i=0;i<4;i++){let lane=S[i%5][(i/5)|0];
54
+ for(let b=0;b<8;b++){out[i*8+b]=Number(lane&0xffn);lane>>=8n;}}
55
+ return out;
56
+ }
57
+ const utf8=s=>new TextEncoder().encode(s);
58
+ const bytesToBig=u8=>u8.reduce((a,b)=>(a<<8n)|BigInt(b),0n);
59
+ function bigTo32(x){const u=new Uint8Array(32);for(let i=31;i>=0;i--){u[i]=Number(x&0xffn);x>>=8n;}return u;}
60
+ const hex=u8=>[...u8].map(b=>b.toString(16).padStart(2,"0")).join("");
61
+
62
+ /* ---------------- secp256k1 ---------------- */
63
+ const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
64
+ const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
65
+ const Gx= 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n;
66
+ const Gy= 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n;
67
+ const G = {x:Gx, y:Gy};
68
+ const O = null; // point at infinity
69
+
70
+ const mod=(a,m)=>((a%m)+m)%m;
71
+ 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;}
72
+ const inv=(a,m)=>powmod(mod(a,m),m-2n,m);
73
+
74
+ function onCurve(Q){ if(Q===O)return true;
75
+ return mod(Q.y*Q.y - (Q.x*Q.x*Q.x + 7n), P)===0n; }
76
+ function ecAdd(A,B){
77
+ if(A===O)return B; if(B===O)return A;
78
+ if(A.x===B.x){
79
+ if(mod(A.y+B.y,P)===0n) return O;
80
+ // doubling
81
+ const l=mod(3n*A.x*A.x * inv(2n*A.y,P), P);
82
+ const x=mod(l*l - 2n*A.x, P);
83
+ return {x, y: mod(l*(A.x-x)-A.y, P)};
84
+ }
85
+ const l=mod((B.y-A.y)*inv(B.x-A.x,P), P);
86
+ const x=mod(l*l - A.x - B.x, P);
87
+ return {x, y: mod(l*(A.x-x)-A.y, P)};
88
+ }
89
+ /* Scalar multiplication in Jacobian coordinates: one modular inversion per
90
+ multiplication instead of one per step (~10x faster than affine). Same
91
+ results as the affine version; ecAdd/onCurve/serialization unchanged. */
92
+ function jDouble(X,Y,Z){
93
+ if(Y===0n) return [0n,1n,0n];
94
+ const A=mod(X*X,P), B=mod(Y*Y,P), C=mod(B*B,P);
95
+ const D=mod(2n*(mod((X+B)*(X+B),P)-A-C),P);
96
+ const E=mod(3n*A,P), F=mod(E*E,P);
97
+ const X3=mod(F-2n*D,P);
98
+ return [X3, mod(E*(D-X3)-8n*C,P), mod(2n*Y*Z,P)];
99
+ }
100
+ function jAdd(X1,Y1,Z1,X2,Y2,Z2){
101
+ if(Z1===0n) return [X2,Y2,Z2]; if(Z2===0n) return [X1,Y1,Z1];
102
+ const Z1Z1=mod(Z1*Z1,P), Z2Z2=mod(Z2*Z2,P);
103
+ const U1=mod(X1*Z2Z2,P), U2=mod(X2*Z1Z1,P);
104
+ const S1=mod(Y1*Z2*Z2Z2,P), S2=mod(Y2*Z1*Z1Z1,P);
105
+ const Hh=mod(U2-U1,P), r=mod(S2-S1,P);
106
+ if(Hh===0n){ return r===0n ? jDouble(X1,Y1,Z1) : [0n,1n,0n]; }
107
+ const HH=mod(Hh*Hh,P), HHH=mod(HH*Hh,P), V=mod(U1*HH,P);
108
+ const X3=mod(r*r-HHH-2n*V,P);
109
+ return [X3, mod(r*(V-X3)-S1*HHH,P), mod(Z1*Z2*Hh,P)];
110
+ }
111
+ function ecMul(k,Q){
112
+ k=mod(k,N); if(Q===O||k===0n) return O;
113
+ let R=[0n,1n,0n], T=[Q.x,Q.y,1n];
114
+ 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; }
115
+ if(R[2]===0n) return O;
116
+ const zi=inv(R[2],P), zi2=mod(zi*zi,P);
117
+ return {x:mod(R[0]*zi2,P), y:mod(R[1]*zi2*zi,P)};
118
+ }
119
+ function ecNeg(Q){ return Q===O?O:{x:Q.x, y:mod(-Q.y,P)}; }
120
+
121
+ /* Ethereum-style address of a point: last 20 bytes of keccak(x||y).
122
+ Used by the on-chain verifier (ecrecover trick compares addresses). */
123
+ function pointAddr(Q){
124
+ const u=new Uint8Array(64); u.set(bigTo32(Q.x),0); u.set(bigTo32(Q.y),32);
125
+ return "0x"+hex(keccak256(u).slice(12));
126
+ }
127
+
128
+ /* ---------------- H: second generator, nothing-up-my-sleeve ----------------
129
+ Try-and-increment from a fixed tag; discrete log of H wrt G unknown. */
130
+ function deriveH(){
131
+ for(let i=0;;i++){
132
+ const x=mod(bytesToBig(keccak256(utf8("zkThunder/H/v1/"+i))),P);
133
+ const y2=mod(x*x*x+7n,P);
134
+ const y=powmod(y2,(P+1n)/4n,P); // p ≡ 3 mod 4
135
+ if(mod(y*y,P)===y2){ return {x, y: (y&1n)?mod(-y,P):y}; } // even y
136
+ }
137
+ }
138
+ const H=deriveH();
139
+
140
+ /* ---------------- randomness ---------------- */
141
+ const nodeCrypto=require("crypto");
142
+ function randScalar(){
143
+ while(true){
144
+ const k=bytesToBig(new Uint8Array(nodeCrypto.randomBytes(32)));
145
+ const r=mod(k,N); if(r>0n) return r;
146
+ }
147
+ }
148
+
149
+ /* ---------------- Fiat-Shamir challenge ---------------- */
150
+ function challenge(tag, points, extra32s){
151
+ const parts=[utf8(tag)];
152
+ for(const Q of points){ parts.push(bigTo32(Q.x), bigTo32(Q.y)); }
153
+ for(const e of (extra32s||[])) parts.push(e);
154
+ const total=parts.reduce((s,p2)=>s+p2.length,0);
155
+ const buf=new Uint8Array(total); let o=0;
156
+ for(const p2 of parts){ buf.set(p2,o); o+=p2.length; }
157
+ return mod(bytesToBig(keccak256(buf)), N);
158
+ }
159
+
160
+ /* ---------------- Pedersen commitment ----------------
161
+ Commit to a 32-byte value (e.g. a state root). The value is reduced
162
+ mod n; binding is to (value mod n). C = x*G + r*H. */
163
+ function commit(value32, r){
164
+ const x=mod(bytesToBig(value32), N);
165
+ r = r===undefined ? randScalar() : r;
166
+ return { C: ecAdd(ecMul(x,G), ecMul(r,H)), x, r };
167
+ }
168
+
169
+ /* ---------------- NIZK: proof of opening (Okamoto) ----------------
170
+ Proves knowledge of (x, r) with C = xG + rH, revealing neither. */
171
+ const TAG_OPEN="zkThunder/sigma/open/v1";
172
+ function proveOpening(C, x, r, ctx32){
173
+ const a=randScalar(), b=randScalar();
174
+ const A=ecAdd(ecMul(a,G), ecMul(b,H));
175
+ const c=challenge(TAG_OPEN,[C,A],[ctx32]);
176
+ return { A, z1: mod(a+c*x,N), z2: mod(b+c*r,N) };
177
+ }
178
+ function verifyOpening(C, prf, ctx32){
179
+ if(!onCurve(C)||!onCurve(prf.A)) return false;
180
+ const c=challenge(TAG_OPEN,[C,prf.A],[ctx32]);
181
+ const L=ecAdd(ecMul(prf.z1,G), ecMul(prf.z2,H));
182
+ const R=ecAdd(prf.A, ecMul(c,C));
183
+ return L!==O && R!==O && L.x===R.x && L.y===R.y;
184
+ }
185
+
186
+ /* ---------------- NIZK: equality of committed values ----------------
187
+ C1 = xG + r1H and C2 = xG + r2H hide the SAME x ⇔
188
+ D = C1 - C2 = (r1-r2)H. Schnorr proof of knowledge of dlog of D
189
+ base H. Reveals nothing about x. */
190
+ const TAG_EQ="zkThunder/sigma/eq/v1";
191
+ function proveEquality(C1, r1, C2, r2, ctx32){
192
+ const rho=mod(r1-r2,N);
193
+ const k=randScalar();
194
+ const A=ecMul(k,H);
195
+ const c=challenge(TAG_EQ,[C1,C2,A],[ctx32]);
196
+ return { A, z: mod(k+c*rho,N) };
197
+ }
198
+ function verifyEquality(C1, C2, prf, ctx32){
199
+ if(!onCurve(C1)||!onCurve(C2)||!onCurve(prf.A)) return false;
200
+ const D=ecAdd(C1, ecNeg(C2));
201
+ if(D===O) return false; // identical commitments: reject
202
+ const c=challenge(TAG_EQ,[C1,C2,prf.A],[ctx32]);
203
+ const L=ecMul(prf.z,H);
204
+ const R=ecAdd(prf.A, ecMul(c,D));
205
+ return L!==O && R!==O && L.x===R.x && L.y===R.y;
206
+ }
207
+
208
+ /* ---------------- serialization (for on-chain + transport) ---------------- */
209
+ function pt(Qp){ return { x:"0x"+Qp.x.toString(16).padStart(64,"0"),
210
+ y:"0x"+Qp.y.toString(16).padStart(64,"0") }; }
211
+ function sc(s){ return "0x"+s.toString(16).padStart(64,"0"); }
212
+
213
+ module.exports={ keccak256, utf8, hex, bigTo32, bytesToBig,
214
+ P,N,G,H,O, mod, inv, onCurve, ecAdd, ecMul, ecNeg, pointAddr,
215
+ randScalar, challenge, commit,
216
+ proveOpening, verifyOpening, proveEquality, verifyEquality,
217
+ pt, sc, TAG_OPEN, TAG_EQ };
218
+
219
+ /* ---------------- NIZK: opening to a PUBLIC value ----------------
220
+ Proves C = v*G + r*H for a public v (e.g. a block's state root), i.e.
221
+ knowledge of r with C - v*G = r*H. Schnorr on base H. The contract
222
+ computes v*G itself (via the ecrecover trick on the prover-supplied
223
+ point) so the prover cannot substitute a different value. */
224
+ const TAG_PUB="zkThunder/sigma/pub/v1";
225
+ function proveOpeningToPublic(C, v, r, ctx32){
226
+ const vG=ecMul(mod(bytesToBig(v),N),G);
227
+ const D=ecAdd(C, ecNeg(vG)); // = r*H
228
+ const k=randScalar();
229
+ const A=ecMul(k,H);
230
+ const c=challenge(TAG_PUB,[C,vG,A],[ctx32]);
231
+ return { vG, A, z: mod(k+c*r,N), CD: ecMul(c,D) };
232
+ }
233
+ function verifyOpeningToPublic(C, v, prf, ctx32){
234
+ const vG=ecMul(mod(bytesToBig(v),N),G);
235
+ if(pointAddr(vG)!==pointAddr(prf.vG)) return false;
236
+ const D=ecAdd(C, ecNeg(vG)); if(D===O) return false;
237
+ const c=challenge(TAG_PUB,[C,vG,prf.A],[ctx32]);
238
+ if(pointAddr(ecMul(c,D))!==pointAddr(prf.CD)) return false;
239
+ const R=ecAdd(prf.A, prf.CD);
240
+ return pointAddr(ecMul(prf.z,H))===pointAddr(R);
241
+ }
242
+ module.exports.proveOpeningToPublic=proveOpeningToPublic;
243
+ module.exports.verifyOpeningToPublic=verifyOpeningToPublic;
244
+ module.exports.TAG_PUB=TAG_PUB;
@@ -0,0 +1,53 @@
1
+ // GENERATED COPY of zk/trustless.js from the zkThunder protocol. Do not edit here; run "npm run sync" in sdk/.
2
+ // Trustless anchoring bundle: the lineage bundle (23 words) followed by an
3
+ // opening-to-public proof (7 words) binding C_out to the window end block's
4
+ // REAL state root, which the contract extracts from the block header and
5
+ // checks against blockhash(endBlock).
6
+ // words[0..22] lineage bundle (zk/prove.js)
7
+ // words[23..29] opening of C_out to the header state root (vG, A, z, CD)
8
+ // words[30..36] opening of C_in to the previous window's stored root
9
+ "use strict";
10
+ const S = require("./sigma.js");
11
+ const { proveBundle, verifyBundleJS } = require("./prove.js");
12
+
13
+ /** Build a full trustless bundle with an openable C_out. */
14
+ function proveTrustlessFull(rootPrev, rootOut, ctx32) {
15
+ // Recreate prove.js's construction with access to C_out's blinding.
16
+ const prev = S.commit(rootPrev), cin = S.commit(rootPrev), cout = S.commit(rootOut);
17
+ const { challengeOpen, challengeEq } = require("./prove.js");
18
+ const open = (cm) => {
19
+ const a = S.randScalar(), bb = S.randScalar();
20
+ const A = S.ecAdd(S.ecMul(a, S.G), S.ecMul(bb, S.H));
21
+ const c = challengeOpen(cm.C, A, ctx32);
22
+ return { A, z1: S.mod(a + c * cm.x, S.N), z2: S.mod(bb + c * cm.r, S.N), CC: S.ecMul(c, cm.C) };
23
+ };
24
+ const oi = open(cin), oo = open(cout);
25
+ const rho = S.mod(prev.r - cin.r, S.N), k = S.randScalar(), Aeq = S.ecMul(k, S.H);
26
+ const ceq = challengeEq(prev.C, cin.C, Aeq, ctx32);
27
+ const zeq = S.mod(k + ceq * rho, S.N);
28
+ const D = S.ecAdd(prev.C, S.ecNeg(cin.C)), CD = S.ecMul(ceq, D);
29
+ const pubOut = S.proveOpeningToPublic(cout.C, rootOut, cout.r, ctx32);
30
+ const pubIn = S.proveOpeningToPublic(cin.C, rootPrev, cin.r, ctx32);
31
+ const pub = pubOut;
32
+ const words = [
33
+ cin.C.x, cin.C.y, cout.C.x, cout.C.y, prev.C.x, prev.C.y,
34
+ oi.A.x, oi.A.y, oi.z1, oi.z2, oi.CC.x, oi.CC.y,
35
+ oo.A.x, oo.A.y, oo.z1, oo.z2, oo.CC.x, oo.CC.y,
36
+ Aeq.x, Aeq.y, zeq, CD.x, CD.y,
37
+ pub.vG.x, pub.vG.y, pub.A.x, pub.A.y, pub.z, pub.CD.x, pub.CD.y,
38
+ pubIn.vG.x, pubIn.vG.y, pubIn.A.x, pubIn.A.y, pubIn.z, pubIn.CD.x, pubIn.CD.y,
39
+ ];
40
+ const buf = new Uint8Array(37 * 32);
41
+ words.forEach((v, i) => buf.set(S.bigTo32(v), i * 32));
42
+ return { words, hex: "0x" + S.hex(buf), commitments: { prevCout: prev.C, C_in: cin.C, C_out: cout.C } };
43
+ }
44
+
45
+ /** Mirror of TrustlessAnchor's checks on the bundle (given the public root). */
46
+ function verifyTrustlessJS(words, ctx32, rootPrev, rootOut) {
47
+ if (words.length !== 37) return false;
48
+ if (!verifyBundleJS(words.slice(0, 23), ctx32)) return false;
49
+ const pt = (i) => ({ x: words[i], y: words[i + 1] });
50
+ if (!S.verifyOpeningToPublic(pt(2), rootOut, { vG: pt(23), A: pt(25), z: words[27], CD: pt(28) }, ctx32)) return false;
51
+ return S.verifyOpeningToPublic(pt(0), rootPrev, { vG: pt(30), A: pt(32), z: words[34], CD: pt(35) }, ctx32);
52
+ }
53
+ module.exports = { proveTrustlessFull, verifyTrustlessJS };
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@zkthunder_/sdk",
3
+ "version": "0.1.0",
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
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "files": ["index.js", "index.d.ts", "lib/", "README.md"],
8
+ "scripts": {
9
+ "test": "node test.js",
10
+ "sync": "node sync.js"
11
+ },
12
+ "keywords": ["zero-knowledge", "pedersen", "range-proof", "robinhood-chain", "zkthunder", "secp256k1"],
13
+ "license": "Apache-2.0",
14
+ "engines": { "node": ">=18" }
15
+ }