@soteria1/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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/dist/confidential/confidential.d.ts +142 -0
  4. package/dist/confidential/confidential.js +266 -0
  5. package/dist/confidential/index.d.ts +1 -0
  6. package/dist/confidential/index.js +1 -0
  7. package/dist/index.d.ts +24 -0
  8. package/dist/index.js +24 -0
  9. package/dist/pool/crypto.d.ts +17 -0
  10. package/dist/pool/crypto.js +79 -0
  11. package/dist/pool/index.d.ts +4 -0
  12. package/dist/pool/index.js +4 -0
  13. package/dist/pool/note.d.ts +22 -0
  14. package/dist/pool/note.js +66 -0
  15. package/dist/pool/pdas.d.ts +54 -0
  16. package/dist/pool/pdas.js +109 -0
  17. package/dist/pool/prover.d.ts +44 -0
  18. package/dist/pool/prover.js +73 -0
  19. package/dist/shielded/index.d.ts +5 -0
  20. package/dist/shielded/index.js +5 -0
  21. package/dist/shielded/instruction.d.ts +20 -0
  22. package/dist/shielded/instruction.js +50 -0
  23. package/dist/shielded/keypair.d.ts +23 -0
  24. package/dist/shielded/keypair.js +72 -0
  25. package/dist/shielded/note.d.ts +22 -0
  26. package/dist/shielded/note.js +32 -0
  27. package/dist/shielded/prover.d.ts +46 -0
  28. package/dist/shielded/prover.js +90 -0
  29. package/dist/shielded/scan.d.ts +21 -0
  30. package/dist/shielded/scan.js +28 -0
  31. package/dist/stealth/index.d.ts +2 -0
  32. package/dist/stealth/index.js +2 -0
  33. package/dist/stealth/scanner.d.ts +20 -0
  34. package/dist/stealth/scanner.js +31 -0
  35. package/dist/stealth/stealth.d.ts +46 -0
  36. package/dist/stealth/stealth.js +119 -0
  37. package/dist/zk/index.d.ts +2 -0
  38. package/dist/zk/index.js +2 -0
  39. package/dist/zk/merkle.d.ts +36 -0
  40. package/dist/zk/merkle.js +92 -0
  41. package/dist/zk/prover.d.ts +50 -0
  42. package/dist/zk/prover.js +87 -0
  43. package/package.json +65 -0
@@ -0,0 +1,90 @@
1
+ import { groth16 } from "snarkjs";
2
+ import { keccak_256 } from "@noble/hashes/sha3";
3
+ import { FIELD } from "./keypair.js";
4
+ import { newNote, commitment, nullifier, encryptNoteSecret } from "./note.js";
5
+ // BN254 base field (for negating proof.A).
6
+ const Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
7
+ const LEVELS = 20;
8
+ function be32(v) {
9
+ let x = BigInt(v);
10
+ const o = new Array(32).fill(0);
11
+ for (let i = 31; i >= 0; i--) {
12
+ o[i] = Number(x & 0xffn);
13
+ x >>= 8n;
14
+ }
15
+ return o;
16
+ }
17
+ // keccak(recipient || relayer || extAmount_le_i64 || fee_le_u64), masked below
18
+ // the field — MUST byte-match the program's ext_data_hash.
19
+ export function extDataHash(recipient, relayer, extAmount, fee) {
20
+ const buf = new Uint8Array(80);
21
+ buf.set(recipient.toBytes(), 0);
22
+ buf.set(relayer.toBytes(), 32);
23
+ const ea = BigInt.asUintN(64, extAmount);
24
+ const fe = BigInt.asUintN(64, fee);
25
+ for (let i = 0; i < 8; i++) {
26
+ buf[64 + i] = Number((ea >> BigInt(8 * i)) & 0xffn);
27
+ buf[72 + i] = Number((fe >> BigInt(8 * i)) & 0xffn);
28
+ }
29
+ const hsh = keccak_256(buf);
30
+ hsh[0] &= 0x1f;
31
+ let v = 0n;
32
+ for (const b of hsh)
33
+ v = (v << 8n) | BigInt(b);
34
+ return v;
35
+ }
36
+ /** publicAmount = (extAmount - fee) mod p. */
37
+ export function publicAmount(extAmount, fee) {
38
+ let v = extAmount - fee;
39
+ if (v < 0n)
40
+ v += FIELD;
41
+ return v % FIELD;
42
+ }
43
+ const zeros = (n) => Array(n).fill("0");
44
+ /**
45
+ * Build a hidden-amount transaction proof for the on-chain `transact`
46
+ * instruction. Caller must ensure Σinputs + extAmount == Σoutputs + fee.
47
+ */
48
+ export async function buildTransaction(p) {
49
+ const sk = p.spendKeypair;
50
+ const inputs = [...p.inputs];
51
+ while (inputs.length < 2) {
52
+ inputs.push({ note: newNote(0n, sk.publicKey), pathElements: zeros(LEVELS).map(BigInt), pathIndices: Array(LEVELS).fill(0) });
53
+ }
54
+ const outputs = [...p.outputs];
55
+ while (outputs.length < 2)
56
+ outputs.push({ note: newNote(0n, sk.publicKey), encPub: sk.encPub });
57
+ const inNullifiers = await Promise.all(inputs.map((i) => nullifier(i.note, sk.privateKey)));
58
+ const outCommitments = await Promise.all(outputs.map((o) => commitment(o.note)));
59
+ const encryptedSecrets = await Promise.all(outputs.map((o) => encryptNoteSecret(o.note, o.encPub)));
60
+ const witness = {
61
+ root: p.root.toString(),
62
+ publicAmount: publicAmount(p.extAmount, p.fee).toString(),
63
+ extDataHash: extDataHash(p.recipient, p.relayer, p.extAmount, p.fee).toString(),
64
+ inputNullifier: inNullifiers.map(String),
65
+ outputCommitment: outCommitments.map(String),
66
+ inAmount: inputs.map((i) => i.note.amount.toString()),
67
+ inPrivateKey: inputs.map(() => sk.privateKey.toString()),
68
+ inBlinding: inputs.map((i) => i.note.blinding.toString()),
69
+ inPathIndices: inputs.map((i) => i.pathIndices.map(String)),
70
+ inPathElements: inputs.map((i) => i.pathElements.map(String)),
71
+ outAmount: outputs.map((o) => o.note.amount.toString()),
72
+ outPubkey: outputs.map((o) => o.note.pubkey.toString()),
73
+ outBlinding: outputs.map((o) => o.note.blinding.toString()),
74
+ };
75
+ const { proof, publicSignals } = await groth16.fullProve(witness, p.wasmPath, p.zkeyPath);
76
+ const ax = BigInt(proof.pi_a[0]);
77
+ const ay = (Q - (BigInt(proof.pi_a[1]) % Q)) % Q;
78
+ return {
79
+ proofA: [...be32(ax), ...be32(ay)],
80
+ proofB: [
81
+ ...be32(proof.pi_b[0][1]), ...be32(proof.pi_b[0][0]),
82
+ ...be32(proof.pi_b[1][1]), ...be32(proof.pi_b[1][0]),
83
+ ],
84
+ proofC: [...be32(proof.pi_c[0]), ...be32(proof.pi_c[1])],
85
+ publicInputs: publicSignals.map((s) => be32(s)),
86
+ nullifiers: inNullifiers,
87
+ outputCommitments: outCommitments,
88
+ encryptedSecrets,
89
+ };
90
+ }
@@ -0,0 +1,21 @@
1
+ import { ShieldedKeypair } from "./keypair.js";
2
+ import { Note } from "./note.js";
3
+ /** One output emitted by `transact`, paired with its encrypted secret. */
4
+ export interface OutputRecord {
5
+ commitment: bigint;
6
+ encryptedSecret: string;
7
+ leafIndex: number;
8
+ }
9
+ export interface OwnedNote extends Note {
10
+ leafIndex: number;
11
+ nullifier: bigint;
12
+ }
13
+ /**
14
+ * Scan emitted outputs for notes owned by `kp`: decrypt each secret, and keep it
15
+ * if the reconstructed commitment matches and the amount is non-zero. Returns
16
+ * spendable UTXOs (callers should drop any whose nullifier is already spent
17
+ * on-chain).
18
+ */
19
+ export declare function scanOutputs(records: OutputRecord[], kp: ShieldedKeypair): Promise<OwnedNote[]>;
20
+ /** Total spendable balance from a set of owned notes. */
21
+ export declare const balance: (notes: OwnedNote[]) => bigint;
@@ -0,0 +1,28 @@
1
+ import { commitment, nullifier, decryptNoteSecret } from "./note.js";
2
+ /**
3
+ * Scan emitted outputs for notes owned by `kp`: decrypt each secret, and keep it
4
+ * if the reconstructed commitment matches and the amount is non-zero. Returns
5
+ * spendable UTXOs (callers should drop any whose nullifier is already spent
6
+ * on-chain).
7
+ */
8
+ export async function scanOutputs(records, kp) {
9
+ const owned = [];
10
+ for (const r of records) {
11
+ let secret;
12
+ try {
13
+ secret = await decryptNoteSecret(r.encryptedSecret, kp.encPriv);
14
+ }
15
+ catch {
16
+ continue; // not encrypted to us
17
+ }
18
+ const note = { amount: secret.amount, pubkey: kp.publicKey, blinding: secret.blinding };
19
+ if (secret.amount === 0n)
20
+ continue;
21
+ if ((await commitment(note)) !== r.commitment)
22
+ continue;
23
+ owned.push({ ...note, leafIndex: r.leafIndex, nullifier: await nullifier(note, kp.privateKey) });
24
+ }
25
+ return owned;
26
+ }
27
+ /** Total spendable balance from a set of owned notes. */
28
+ export const balance = (notes) => notes.reduce((s, n) => s + n.amount, 0n);
@@ -0,0 +1,2 @@
1
+ export * from "./stealth.js";
2
+ export * from "./scanner.js";
@@ -0,0 +1,2 @@
1
+ export * from "./stealth.js";
2
+ export * from "./scanner.js";
@@ -0,0 +1,20 @@
1
+ import { StealthKeys } from "./stealth.js";
2
+ /** One published announcement: the sender's ephemeral key + a view tag. */
3
+ export interface Announcement {
4
+ ephemeralPub: Uint8Array;
5
+ viewTag: number;
6
+ stealthPub?: Uint8Array;
7
+ slot?: number;
8
+ signature?: string;
9
+ }
10
+ export interface DetectedPayment {
11
+ stealthPub: Uint8Array;
12
+ stealthScalar: bigint;
13
+ announcement: Announcement;
14
+ }
15
+ /**
16
+ * Scan a batch of announcements for payments addressed to `keys`.
17
+ * The view tag lets us reject ~255/256 of non-matching announcements with a
18
+ * single hash before doing the full point recovery.
19
+ */
20
+ export declare function scanAnnouncements(keys: StealthKeys, announcements: Announcement[]): DetectedPayment[];
@@ -0,0 +1,31 @@
1
+ import { recoverStealth } from "./stealth.js";
2
+ /**
3
+ * Scan a batch of announcements for payments addressed to `keys`.
4
+ * The view tag lets us reject ~255/256 of non-matching announcements with a
5
+ * single hash before doing the full point recovery.
6
+ */
7
+ export function scanAnnouncements(keys, announcements) {
8
+ const found = [];
9
+ for (const ann of announcements) {
10
+ const res = recoverStealth(keys, ann.ephemeralPub, ann.viewTag);
11
+ if (!res)
12
+ continue;
13
+ // If the registry recorded the destination, confirm it matches ours.
14
+ if (ann.stealthPub && !equal(ann.stealthPub, res.stealthPub))
15
+ continue;
16
+ found.push({
17
+ stealthPub: res.stealthPub,
18
+ stealthScalar: res.stealthScalar,
19
+ announcement: ann,
20
+ });
21
+ }
22
+ return found;
23
+ }
24
+ function equal(a, b) {
25
+ if (a.length !== b.length)
26
+ return false;
27
+ let diff = 0;
28
+ for (let i = 0; i < a.length; i++)
29
+ diff |= a[i] ^ b[i];
30
+ return diff === 0;
31
+ }
@@ -0,0 +1,46 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ export interface MetaAddress {
3
+ spendPub: Uint8Array;
4
+ viewPub: Uint8Array;
5
+ }
6
+ export interface StealthKeys {
7
+ spendScalar: bigint;
8
+ viewScalar: bigint;
9
+ meta: MetaAddress;
10
+ }
11
+ export interface StealthOutput {
12
+ stealthPub: Uint8Array;
13
+ stealthAddress: PublicKey;
14
+ ephemeralPub: Uint8Array;
15
+ viewTag: number;
16
+ }
17
+ /** Generate a recipient's stealth key material + shareable meta-address. */
18
+ export declare function generateStealthKeys(): StealthKeys;
19
+ /**
20
+ * Derive a recipient's stealth keys deterministically from a `seed` — typically
21
+ * the recipient's signature over a fixed message from their main wallet.
22
+ *
23
+ * Because the same seed always yields the same keys, the recipient recovers
24
+ * their entire stealth identity from their wallet alone: nothing is stored, and
25
+ * losing local state loses nothing. Spend and view scalars are domain-separated
26
+ * so the view key (shareable with a scanning service) never leaks the spend key.
27
+ */
28
+ export declare function deriveStealthKeys(seed: Uint8Array): StealthKeys;
29
+ /**
30
+ * SENDER: derive a one-time stealth address for a recipient's meta-address.
31
+ * Publish `ephemeralPub` (R) and `viewTag` so the recipient can detect it.
32
+ */
33
+ export declare function deriveStealthAddress(meta: MetaAddress): StealthOutput;
34
+ /**
35
+ * RECIPIENT: given an announced ephemeral key R, recompute the stealth pubkey
36
+ * and (if it's ours) the one-time signing scalar. Returns null if no match.
37
+ */
38
+ export declare function recoverStealth(keys: StealthKeys, ephemeralPub: Uint8Array, expectedViewTag?: number): {
39
+ stealthPub: Uint8Array;
40
+ stealthScalar: bigint;
41
+ } | null;
42
+ /**
43
+ * Sign a message with a recovered stealth scalar (raw-scalar ed25519).
44
+ * Use this in a custom Solana transaction signer to spend from a stealth address.
45
+ */
46
+ export declare function signWithStealthScalar(message: Uint8Array, stealthScalar: bigint): Uint8Array;
@@ -0,0 +1,119 @@
1
+ import { ed25519 } from "@noble/curves/ed25519";
2
+ import { sha512 } from "@noble/hashes/sha512";
3
+ import { bytesToNumberLE, numberToBytesLE } from "@noble/curves/abstract/utils";
4
+ import { PublicKey } from "@solana/web3.js";
5
+ /**
6
+ * Dual-key stealth addresses for Solana (ERC-5564 style, adapted to ed25519).
7
+ *
8
+ * A recipient publishes a META-ADDRESS made of two public keys:
9
+ * - spend public key S = s*G (controls spending)
10
+ * - view public key V = v*G (lets the recipient scan cheaply)
11
+ *
12
+ * A sender, knowing (S, V), derives a fresh one-time stealth address per payment
13
+ * and publishes only an ephemeral public key R. Nobody watching the chain can
14
+ * link the stealth address back to the recipient's meta-address.
15
+ *
16
+ * This module hides WHICH wallet receives. It is not a mixer: funds are not
17
+ * pooled, and there is no deposit/withdraw that breaks the sender->recipient link.
18
+ *
19
+ * ── Spending caveat (read this) ──────────────────────────────────────────────
20
+ * The one-time signing key is a raw scalar `p = (s + tweak) mod L`. Solana's
21
+ * standard Keypair derives its scalar by hashing a 32-byte seed, so you CANNOT
22
+ * load `p` into a normal web3.js Keypair and sign. Spending requires raw-scalar
23
+ * ed25519 signing (Monero-style). `signWithStealthScalar` below does that; wire
24
+ * it into a custom transaction signer when you build the claim flow.
25
+ */
26
+ const L = ed25519.CURVE.n; // group order
27
+ const Base = ed25519.ExtendedPoint.BASE;
28
+ function hashToScalar(...chunks) {
29
+ const h = sha512(concat(chunks));
30
+ return bytesToNumberLE(h) % L;
31
+ }
32
+ function concat(parts) {
33
+ const len = parts.reduce((n, p) => n + p.length, 0);
34
+ const out = new Uint8Array(len);
35
+ let o = 0;
36
+ for (const p of parts) {
37
+ out.set(p, o);
38
+ o += p.length;
39
+ }
40
+ return out;
41
+ }
42
+ /** Generate a recipient's stealth key material + shareable meta-address. */
43
+ export function generateStealthKeys() {
44
+ const s = bytesToNumberLE(ed25519.utils.randomPrivateKey()) % L;
45
+ const v = bytesToNumberLE(ed25519.utils.randomPrivateKey()) % L;
46
+ const spendPub = Base.multiply(s).toRawBytes();
47
+ const viewPub = Base.multiply(v).toRawBytes();
48
+ return { spendScalar: s, viewScalar: v, meta: { spendPub, viewPub } };
49
+ }
50
+ /**
51
+ * Derive a recipient's stealth keys deterministically from a `seed` — typically
52
+ * the recipient's signature over a fixed message from their main wallet.
53
+ *
54
+ * Because the same seed always yields the same keys, the recipient recovers
55
+ * their entire stealth identity from their wallet alone: nothing is stored, and
56
+ * losing local state loses nothing. Spend and view scalars are domain-separated
57
+ * so the view key (shareable with a scanning service) never leaks the spend key.
58
+ */
59
+ export function deriveStealthKeys(seed) {
60
+ const enc = new TextEncoder();
61
+ const s = hashToScalar(enc.encode("soteria:stealth:spend:v1"), seed);
62
+ const v = hashToScalar(enc.encode("soteria:stealth:view:v1"), seed);
63
+ const spendPub = Base.multiply(s).toRawBytes();
64
+ const viewPub = Base.multiply(v).toRawBytes();
65
+ return { spendScalar: s, viewScalar: v, meta: { spendPub, viewPub } };
66
+ }
67
+ /**
68
+ * SENDER: derive a one-time stealth address for a recipient's meta-address.
69
+ * Publish `ephemeralPub` (R) and `viewTag` so the recipient can detect it.
70
+ */
71
+ export function deriveStealthAddress(meta) {
72
+ const r = bytesToNumberLE(ed25519.utils.randomPrivateKey()) % L;
73
+ const R = Base.multiply(r).toRawBytes();
74
+ const Vpoint = ed25519.ExtendedPoint.fromHex(meta.viewPub);
75
+ const shared = Vpoint.multiply(r).toRawBytes(); // r*V = (r*v)*G
76
+ const tweak = hashToScalar(shared); // common secret both sides can compute
77
+ const Spoint = ed25519.ExtendedPoint.fromHex(meta.spendPub);
78
+ const stealth = Spoint.add(Base.multiply(tweak)); // P = S + tweak*G
79
+ const stealthPub = stealth.toRawBytes();
80
+ const viewTag = sha512(concat([new Uint8Array([0x01]), shared]))[0];
81
+ return {
82
+ stealthPub,
83
+ stealthAddress: new PublicKey(stealthPub),
84
+ ephemeralPub: R,
85
+ viewTag,
86
+ };
87
+ }
88
+ /**
89
+ * RECIPIENT: given an announced ephemeral key R, recompute the stealth pubkey
90
+ * and (if it's ours) the one-time signing scalar. Returns null if no match.
91
+ */
92
+ export function recoverStealth(keys, ephemeralPub, expectedViewTag) {
93
+ const Rpoint = ed25519.ExtendedPoint.fromHex(ephemeralPub);
94
+ const shared = Rpoint.multiply(keys.viewScalar).toRawBytes(); // v*R = (r*v)*G
95
+ if (expectedViewTag !== undefined) {
96
+ const tag = sha512(concat([new Uint8Array([0x01]), shared]))[0];
97
+ if (tag !== expectedViewTag)
98
+ return null; // fast reject
99
+ }
100
+ const tweak = hashToScalar(shared);
101
+ const stealthScalar = (keys.spendScalar + tweak) % L; // p = s + tweak
102
+ const stealthPub = Base.multiply(stealthScalar).toRawBytes(); // must equal P
103
+ return { stealthPub, stealthScalar };
104
+ }
105
+ /**
106
+ * Sign a message with a recovered stealth scalar (raw-scalar ed25519).
107
+ * Use this in a custom Solana transaction signer to spend from a stealth address.
108
+ */
109
+ export function signWithStealthScalar(message, stealthScalar) {
110
+ // Deterministic nonce from a domain-separated hash of (scalar || message).
111
+ const sBytes = numberToBytesLE(stealthScalar, 32);
112
+ const nonce = bytesToNumberLE(sha512(concat([sBytes, message]))) % L;
113
+ const Rpt = Base.multiply(nonce);
114
+ const Rbytes = Rpt.toRawBytes();
115
+ const Abytes = Base.multiply(stealthScalar).toRawBytes();
116
+ const k = bytesToNumberLE(sha512(concat([Rbytes, Abytes, message]))) % L;
117
+ const S = (nonce + k * stealthScalar) % L;
118
+ return concat([Rbytes, numberToBytesLE(S, 32)]);
119
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./merkle.js";
2
+ export * from "./prover.js";
@@ -0,0 +1,2 @@
1
+ export * from "./merkle.js";
2
+ export * from "./prover.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Poseidon Merkle tree whose hashing matches circuits/credential.circom:
3
+ * parent = Poseidon(left, right)
4
+ * leaf = Poseidon(secret) // identity commitment
5
+ *
6
+ * Fixed depth; empty slots are filled with a zero subtree so the root is
7
+ * stable. Keep `depth` in sync with the circuit (default 20).
8
+ */
9
+ export declare class PoseidonMerkleTree {
10
+ private poseidon;
11
+ private F;
12
+ readonly depth: number;
13
+ private zeros;
14
+ private layers;
15
+ private constructor();
16
+ static create(depth?: number): Promise<PoseidonMerkleTree>;
17
+ private toBig;
18
+ hash1(a: bigint): bigint;
19
+ hash2(a: bigint, b: bigint): bigint;
20
+ /** identity commitment for a secret */
21
+ commitment(secret: bigint): bigint;
22
+ /** insert a leaf (already a commitment) and return its index */
23
+ insert(leaf: bigint): number;
24
+ /**
25
+ * Insert many leaves and rebuild once — O(n) total instead of O(n²) from
26
+ * calling insert() in a loop. Use this to load a tree from a commitment list.
27
+ */
28
+ insertMany(leaves: bigint[]): void;
29
+ private rebuild;
30
+ root(): bigint;
31
+ /** Merkle proof for the leaf at `index`, formatted for the circuit. */
32
+ proof(index: number): {
33
+ pathElements: bigint[];
34
+ pathIndices: number[];
35
+ };
36
+ }
@@ -0,0 +1,92 @@
1
+ import { buildPoseidon } from "circomlibjs";
2
+ /**
3
+ * Poseidon Merkle tree whose hashing matches circuits/credential.circom:
4
+ * parent = Poseidon(left, right)
5
+ * leaf = Poseidon(secret) // identity commitment
6
+ *
7
+ * Fixed depth; empty slots are filled with a zero subtree so the root is
8
+ * stable. Keep `depth` in sync with the circuit (default 20).
9
+ */
10
+ export class PoseidonMerkleTree {
11
+ constructor(poseidon, depth) {
12
+ this.zeros = [];
13
+ this.layers = [];
14
+ this.poseidon = poseidon;
15
+ this.F = poseidon.F;
16
+ this.depth = depth;
17
+ }
18
+ static async create(depth = 20) {
19
+ const poseidon = await buildPoseidon();
20
+ const t = new PoseidonMerkleTree(poseidon, depth);
21
+ // precompute zero subtree roots per level
22
+ let cur = 0n;
23
+ t.zeros.push(cur);
24
+ for (let i = 0; i < depth; i++) {
25
+ cur = t.hash2(cur, cur);
26
+ t.zeros.push(cur);
27
+ }
28
+ t.layers = [[]];
29
+ return t;
30
+ }
31
+ toBig(x) {
32
+ return BigInt(this.F.toString(x));
33
+ }
34
+ hash1(a) {
35
+ return this.toBig(this.poseidon([a]));
36
+ }
37
+ hash2(a, b) {
38
+ return this.toBig(this.poseidon([a, b]));
39
+ }
40
+ /** identity commitment for a secret */
41
+ commitment(secret) {
42
+ return this.hash1(secret);
43
+ }
44
+ /** insert a leaf (already a commitment) and return its index */
45
+ insert(leaf) {
46
+ const index = this.layers[0].length;
47
+ this.layers[0].push(leaf);
48
+ this.rebuild();
49
+ return index;
50
+ }
51
+ /**
52
+ * Insert many leaves and rebuild once — O(n) total instead of O(n²) from
53
+ * calling insert() in a loop. Use this to load a tree from a commitment list.
54
+ */
55
+ insertMany(leaves) {
56
+ for (const leaf of leaves)
57
+ this.layers[0].push(leaf);
58
+ this.rebuild();
59
+ }
60
+ rebuild() {
61
+ for (let level = 0; level < this.depth; level++) {
62
+ const cur = this.layers[level];
63
+ const next = [];
64
+ for (let i = 0; i < cur.length; i += 2) {
65
+ const left = cur[i];
66
+ const right = i + 1 < cur.length ? cur[i + 1] : this.zeros[level];
67
+ next.push(this.hash2(left, right));
68
+ }
69
+ this.layers[level + 1] = next;
70
+ }
71
+ }
72
+ root() {
73
+ const top = this.layers[this.depth];
74
+ return top && top.length ? top[0] : this.zeros[this.depth];
75
+ }
76
+ /** Merkle proof for the leaf at `index`, formatted for the circuit. */
77
+ proof(index) {
78
+ const pathElements = [];
79
+ const pathIndices = [];
80
+ let idx = index;
81
+ for (let level = 0; level < this.depth; level++) {
82
+ const cur = this.layers[level] ?? [];
83
+ const isRight = idx % 2;
84
+ const siblingIdx = isRight ? idx - 1 : idx + 1;
85
+ const sibling = siblingIdx < cur.length ? cur[siblingIdx] : this.zeros[level];
86
+ pathElements.push(sibling);
87
+ pathIndices.push(isRight);
88
+ idx = Math.floor(idx / 2);
89
+ }
90
+ return { pathElements, pathIndices };
91
+ }
92
+ }
@@ -0,0 +1,50 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { PoseidonMerkleTree } from "./merkle.js";
3
+ export interface CredentialInputs {
4
+ secret: bigint;
5
+ tree: PoseidonMerkleTree;
6
+ leafIndex: number;
7
+ externalNullifier: bigint;
8
+ signalHash: bigint;
9
+ }
10
+ export interface FormattedProof {
11
+ proofA: number[];
12
+ proofB: number[];
13
+ proofC: number[];
14
+ publicInputs: number[][];
15
+ }
16
+ export interface RawProof {
17
+ proof: {
18
+ pi_a: string[];
19
+ pi_b: string[][];
20
+ pi_c: string[];
21
+ };
22
+ publicSignals: string[];
23
+ }
24
+ /**
25
+ * Generate the raw snarkjs proof + public signals. Use this when submitting via
26
+ * the relay, which formats the bytes server-side. Requires the trusted-setup
27
+ * artifacts (see README).
28
+ */
29
+ export declare function proveCredentialRaw(inputs: CredentialInputs, wasmPath: string, zkeyPath: string): Promise<RawProof>;
30
+ /**
31
+ * Generate a selective-disclosure proof and format it for the on-chain verifier
32
+ * (direct submission). For the relay path, use proveCredentialRaw.
33
+ */
34
+ export declare function proveCredential(inputs: CredentialInputs, wasmPath: string, zkeyPath: string): Promise<FormattedProof>;
35
+ export declare function groupPda(groupId: bigint | number): [PublicKey, number];
36
+ export declare function nullifierPda(groupId: bigint | number, nullifierHash: number[]): [PublicKey, number];
37
+ /**
38
+ * Account set for `verify_proof`, ordered to match the on-chain context.
39
+ * Call as verifyProof(externalNullifier, proofA, proofB, proofC, publicInputs)
40
+ * via @coral-xyz/anchor's IDL client; externalNullifier must equal
41
+ * publicInputs[PI_EXTERNAL_NULLIFIER] or the program rejects with ScopeMismatch.
42
+ */
43
+ export declare function buildVerifyAccounts(payer: PublicKey, groupId: bigint | number, proof: FormattedProof): {
44
+ programId: PublicKey;
45
+ keys: {
46
+ pubkey: PublicKey;
47
+ isSigner: boolean;
48
+ isWritable: boolean;
49
+ }[];
50
+ };
@@ -0,0 +1,87 @@
1
+ import { groth16 } from "snarkjs";
2
+ import { PublicKey, SystemProgram, } from "@solana/web3.js";
3
+ // BN254 base field prime (for negating proof.A, per groth16-solana convention).
4
+ const Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
5
+ const VERIFIER_PROGRAM_ID = new PublicKey("9HNLpUVFX61pX759oy1vuMMwQaQaGnK9KgMyhTrDrRGs");
6
+ function toBE32(dec) {
7
+ let v = BigInt(dec);
8
+ const out = new Array(32).fill(0);
9
+ for (let i = 31; i >= 0; i--) {
10
+ out[i] = Number(v & 0xffn);
11
+ v >>= 8n;
12
+ }
13
+ return out;
14
+ }
15
+ /**
16
+ * Generate the raw snarkjs proof + public signals. Use this when submitting via
17
+ * the relay, which formats the bytes server-side. Requires the trusted-setup
18
+ * artifacts (see README).
19
+ */
20
+ export async function proveCredentialRaw(inputs, wasmPath, zkeyPath) {
21
+ const { secret, tree, leafIndex, externalNullifier, signalHash } = inputs;
22
+ const { pathElements, pathIndices } = tree.proof(leafIndex);
23
+ const witness = {
24
+ secret: secret.toString(),
25
+ pathElements: pathElements.map((x) => x.toString()),
26
+ pathIndices: pathIndices.map((x) => x.toString()),
27
+ merkleRoot: tree.root().toString(),
28
+ externalNullifier: externalNullifier.toString(),
29
+ signalHash: signalHash.toString(),
30
+ };
31
+ const { proof, publicSignals } = await groth16.fullProve(witness, wasmPath, zkeyPath);
32
+ return { proof, publicSignals };
33
+ }
34
+ /**
35
+ * Generate a selective-disclosure proof and format it for the on-chain verifier
36
+ * (direct submission). For the relay path, use proveCredentialRaw.
37
+ */
38
+ export async function proveCredential(inputs, wasmPath, zkeyPath) {
39
+ const { proof, publicSignals } = await proveCredentialRaw(inputs, wasmPath, zkeyPath);
40
+ // --- proof.A : negate y, then x||y big-endian (groth16-solana wants -A) ---
41
+ const ax = BigInt(proof.pi_a[0]);
42
+ const ay = (Q - (BigInt(proof.pi_a[1]) % Q)) % Q;
43
+ const proofA = [...toBE32(ax), ...toBE32(ay)];
44
+ // --- proof.B : G2, swap the c0/c1 ordering snarkjs emits ---
45
+ const proofB = [
46
+ ...toBE32(proof.pi_b[0][1]),
47
+ ...toBE32(proof.pi_b[0][0]),
48
+ ...toBE32(proof.pi_b[1][1]),
49
+ ...toBE32(proof.pi_b[1][0]),
50
+ ];
51
+ // --- proof.C : x||y big-endian ---
52
+ const proofC = [...toBE32(proof.pi_c[0]), ...toBE32(proof.pi_c[1])];
53
+ // publicSignals order = [outputs..., publicInputs...]
54
+ // = [nullifierHash, merkleRoot, externalNullifier, signalHash]
55
+ const publicInputs = publicSignals.map((s) => toBE32(s));
56
+ return { proofA, proofB, proofC, publicInputs };
57
+ }
58
+ function u64le(n) {
59
+ const b = Buffer.alloc(8);
60
+ b.writeBigUInt64LE(BigInt(n));
61
+ return b;
62
+ }
63
+ export function groupPda(groupId) {
64
+ return PublicKey.findProgramAddressSync([Buffer.from("group"), u64le(groupId)], VERIFIER_PROGRAM_ID);
65
+ }
66
+ export function nullifierPda(groupId, nullifierHash) {
67
+ return PublicKey.findProgramAddressSync([Buffer.from("nullifier"), u64le(groupId), Buffer.from(nullifierHash)], VERIFIER_PROGRAM_ID);
68
+ }
69
+ /**
70
+ * Account set for `verify_proof`, ordered to match the on-chain context.
71
+ * Call as verifyProof(externalNullifier, proofA, proofB, proofC, publicInputs)
72
+ * via @coral-xyz/anchor's IDL client; externalNullifier must equal
73
+ * publicInputs[PI_EXTERNAL_NULLIFIER] or the program rejects with ScopeMismatch.
74
+ */
75
+ export function buildVerifyAccounts(payer, groupId, proof) {
76
+ const [group] = groupPda(groupId);
77
+ const [nullifier] = nullifierPda(groupId, proof.publicInputs[0]);
78
+ return {
79
+ programId: VERIFIER_PROGRAM_ID,
80
+ keys: [
81
+ { pubkey: payer, isSigner: true, isWritable: true },
82
+ { pubkey: group, isSigner: false, isWritable: false },
83
+ { pubkey: nullifier, isSigner: false, isWritable: true },
84
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
85
+ ],
86
+ };
87
+ }