@zkthunder_/sdk
v0.2.0
Published
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.
Maintainers
Readme
@zkthunder_/sdk
Zero-knowledge proofs for Robinhood Chain, generated locally with no dependencies, plus a tiny read-only client for the deployed zkThunder contracts. Everything the zkthunder.net labs do, as library calls.
- Pedersen commitments on secp256k1: perfectly hiding, binding under discrete log.
- Lineage bundles: two hidden values, proofs they are well formed, and a proof they link.
- Range proofs: a hidden value is at least a public threshold (32 bit range).
- Bound bundles for the trustless lineage: commitments that provably open to public roots.
- Calldata encoders for every deployed contract, and a client that verifies on chain over plain JSON-RPC.
Secrets never leave your process. The outputs are commitments, proofs and booleans.
Install
npm install @zkthunder_/sdkNode 18 or newer (uses fetch and BigInt). No other dependencies.
Private eligibility with PrivateGate
A user proves that a hidden balance is at least a threshold, for a named
purpose, bound to the gate contract and to their own address. The gate stores
the threshold; any contract can then ask isEligible without the balance ever
appearing on chain.
const zk = require("@zkthunder_/sdk");
const GATE = "0x..."; // a deployed PrivateGate
const user = "0xYourAddress";
const ctx = zk.gateContext(GATE, user, "tier-1 access"); // keccak256(abi.encode(gate, user, purpose))
const { proof, commitment } = zk.proveRange({ value: 1250n, threshold: 1000n, ctx });
// send from `user`: to = GATE, data = zk.encodeGateProve("tier-1 access", proof)
// afterwards, from anywhere:
const chain = new zk.Chain();
await chain.isEligible(GATE, user, "tier-1 access", 1000n); // true, and the 1250 was never revealedThe proof is useless anywhere else: another address, another gate or another purpose changes the context, and the verifier folds the context into every challenge.
Verify a range proof against the deployed verifier
const ctx = zk.context("my-app", "proof-of-reserves", "2026-09");
const rg = zk.proveRange({ value: 5_000_000n, threshold: 1_000_000n, ctx });
await new zk.Chain().verifyRange(rg.proof, ctx); // true: the RangeVerifier on mainnet accepts itTo publish a public receipt from your own address, send
zk.encodeAttest(rg.proof, ctx) to zk.MAINNET.rangeVerifier; it emits
RangeVerified(by, ctx, threshold, commitmentDigest).
Read and audit the trustless lineage
const chain = new zk.Chain();
const head = await chain.lineage(); // { lastSeq, lastEndBlock, lastRoot, latestBlock }
const audit = await chain.auditAnchor(); // re-reads the end block from the node and compares
console.log(audit.ok, audit.rootMatchesBlock, audit.linksToPrevious);Every root in the lineage was extracted by the contract from a block header it checked against the chain's own block hash. No caller ever supplies a root.
Lineage bundles and the public registry
const ctx = zk.context("my-app/lineage", String(Date.now()));
const lin = zk.proveLineage("previous state", "new state", ctx);
await new zk.Chain().verifyLineage(lin.proof, ctx); // LineageVerifier says yes
// record it: send zk.encodeSubmit(lin.proof, ctx) to zk.MAINNET.publicProofsV2: full-coordinate verifiers on BN254
The same constructions on the BN254 curve, where the EVM has built-in point addition and multiplication, so the V2 verifiers compute every scalar multiplication themselves and compare whole points instead of 160 bit addresses. Deployed on mainnet on 13 September 2026; the anchors and the gate still use the V1 verifiers until the audit has compared the two, so use V2 for your own verification and keep V1 for anything that feeds the live contracts. V1 and V2 proofs are not interchangeable.
const rg = zk.proveRangeV2({ value: 1250n, threshold: 1000n, ctx: zk.context("my-app", "balance-check") });
await new zk.Chain().verifyRangeV2(rg.proof, rg.ctx); // true, checked on whole curve points
const lin = zk.proveLineageV2("previous state", "new state", ctx); // 17 words instead of 23
await new zk.Chain().verifyLineageV2(lin.proof, ctx);API
| Function | Purpose |
| --- | --- |
| commit(value, blinding?) | Pedersen commitment to a 32 byte value (text is hashed) |
| proveLineage(prev, out, ctx) / verifyLineage(proof, ctx) | 23 word bundle for LineageVerifier |
| proveRange({ value, threshold, ctx, blinding? }) / verifyRange(proof, ctx) | range proof for RangeVerifier |
| proveTrustless(prev, out, ctx) / verifyTrustless(proof, ctx, prev, out) | 37 word bound bundle for TrustlessAnchor |
| commitV2, proveLineageV2 / verifyLineageV2, proveRangeV2 / verifyRangeV2, proveTrustlessV2 / verifyTrustlessV2 | the same on BN254 for the V2 verifiers (17, 291 and 23 words) |
| context(...parts), purposeId(name), gateContext(gate, user, purpose) | context words that bind proofs to their use |
| encodeVerify, encodeVerifyBound, encodeSubmit, encodeAttest, encodeGateProve, encodeIsEligible | calldata for the deployed contracts |
| new Chain({ rpcUrl?, addresses? }) | verifyLineage, verifyRange, verifyTrustless, verifyLineageV2, verifyRangeV2, verifyTrustlessV2, isEligible, lineage, record, auditAnchor |
| MAINNET | deployed addresses on chain 4663 |
Sending transactions is left to whatever wallet library you already use: the
encoders return calldata, and MAINNET has the addresses.
Limits, stated plainly
- Range proofs cover a 32 bit difference between value and threshold and cost about 1.9 million gas to verify on chain. Zero is not a valid threshold.
- The V1 verifiers, which the live anchors and the gate use, compare 160 bit point addresses (the ecrecover trick). The V2 verifiers check whole points on BN254 and are deployed; the anchors switch to them after the audit.
- These proofs establish commitment structure and lineage linkage. They do not prove a state transition was computed correctly; that is the zkVM tier.
Test
node test.jsRuns the local checks, then verifies fresh proofs against the mainnet contracts when the RPC is reachable.
Apache-2.0.
