@utxopia/sdk 0.1.0-alpha.1

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/package.json +108 -0
  4. package/packages/btc-client/src/esplora-client.ts +153 -0
  5. package/packages/btc-client/src/index.ts +3 -0
  6. package/packages/btc-client/src/op-return.ts +93 -0
  7. package/packages/btc-client/src/types.ts +112 -0
  8. package/packages/sdk/README.md +277 -0
  9. package/packages/sdk/dist/announcement-client.d.ts +64 -0
  10. package/packages/sdk/dist/announcement-client.js +337 -0
  11. package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
  12. package/packages/sdk/dist/auditor-ciphertext.js +135 -0
  13. package/packages/sdk/dist/auditor.d.ts +115 -0
  14. package/packages/sdk/dist/auditor.js +292 -0
  15. package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
  16. package/packages/sdk/dist/bitcoin/ika.js +74 -0
  17. package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
  18. package/packages/sdk/dist/bitcoin/index.js +1 -0
  19. package/packages/sdk/dist/bound-params.d.ts +96 -0
  20. package/packages/sdk/dist/bound-params.js +249 -0
  21. package/packages/sdk/dist/chadbuffer.d.ts +124 -0
  22. package/packages/sdk/dist/chadbuffer.js +405 -0
  23. package/packages/sdk/dist/claim-link.d.ts +30 -0
  24. package/packages/sdk/dist/claim-link.js +50 -0
  25. package/packages/sdk/dist/client.d.ts +233 -0
  26. package/packages/sdk/dist/client.js +416 -0
  27. package/packages/sdk/dist/commitment-tree.d.ts +268 -0
  28. package/packages/sdk/dist/commitment-tree.js +564 -0
  29. package/packages/sdk/dist/config.d.ts +177 -0
  30. package/packages/sdk/dist/config.js +426 -0
  31. package/packages/sdk/dist/core/esplora.d.ts +110 -0
  32. package/packages/sdk/dist/core/esplora.js +187 -0
  33. package/packages/sdk/dist/core/mempool.d.ts +60 -0
  34. package/packages/sdk/dist/core/mempool.js +107 -0
  35. package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
  36. package/packages/sdk/dist/crypto-babyjub.js +312 -0
  37. package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
  38. package/packages/sdk/dist/crypto-ed25519.js +235 -0
  39. package/packages/sdk/dist/crypto.d.ts +59 -0
  40. package/packages/sdk/dist/crypto.js +140 -0
  41. package/packages/sdk/dist/event-client.d.ts +64 -0
  42. package/packages/sdk/dist/event-client.js +165 -0
  43. package/packages/sdk/dist/events.d.ts +127 -0
  44. package/packages/sdk/dist/events.js +305 -0
  45. package/packages/sdk/dist/explorer.d.ts +86 -0
  46. package/packages/sdk/dist/explorer.js +197 -0
  47. package/packages/sdk/dist/index.d.ts +61 -0
  48. package/packages/sdk/dist/index.js +216 -0
  49. package/packages/sdk/dist/instructions.d.ts +1125 -0
  50. package/packages/sdk/dist/instructions.js +1760 -0
  51. package/packages/sdk/dist/keys.d.ts +467 -0
  52. package/packages/sdk/dist/keys.js +799 -0
  53. package/packages/sdk/dist/logger.d.ts +10 -0
  54. package/packages/sdk/dist/logger.js +40 -0
  55. package/packages/sdk/dist/magicblock.d.ts +90 -0
  56. package/packages/sdk/dist/magicblock.js +164 -0
  57. package/packages/sdk/dist/merkle.d.ts +82 -0
  58. package/packages/sdk/dist/merkle.js +141 -0
  59. package/packages/sdk/dist/note.d.ts +348 -0
  60. package/packages/sdk/dist/note.js +483 -0
  61. package/packages/sdk/dist/pda.d.ts +160 -0
  62. package/packages/sdk/dist/pda.js +384 -0
  63. package/packages/sdk/dist/pool-state.d.ts +100 -0
  64. package/packages/sdk/dist/pool-state.js +126 -0
  65. package/packages/sdk/dist/poseidon.d.ts +53 -0
  66. package/packages/sdk/dist/poseidon.js +136 -0
  67. package/packages/sdk/dist/prover/index.d.ts +10 -0
  68. package/packages/sdk/dist/prover/index.js +10 -0
  69. package/packages/sdk/dist/prover/mobile.d.ts +45 -0
  70. package/packages/sdk/dist/prover/mobile.js +227 -0
  71. package/packages/sdk/dist/prover/web.d.ts +136 -0
  72. package/packages/sdk/dist/prover/web.js +597 -0
  73. package/packages/sdk/dist/psbt.d.ts +73 -0
  74. package/packages/sdk/dist/psbt.js +202 -0
  75. package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
  76. package/packages/sdk/dist/selective-disclosure.js +172 -0
  77. package/packages/sdk/dist/sender-memo.d.ts +149 -0
  78. package/packages/sdk/dist/sender-memo.js +250 -0
  79. package/packages/sdk/dist/sns-resolver.d.ts +121 -0
  80. package/packages/sdk/dist/sns-resolver.js +229 -0
  81. package/packages/sdk/dist/solana/connection.d.ts +77 -0
  82. package/packages/sdk/dist/solana/connection.js +133 -0
  83. package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
  84. package/packages/sdk/dist/solana/priority-fee.js +137 -0
  85. package/packages/sdk/dist/stealth.d.ts +429 -0
  86. package/packages/sdk/dist/stealth.js +703 -0
  87. package/packages/sdk/dist/taproot.d.ts +172 -0
  88. package/packages/sdk/dist/taproot.js +494 -0
  89. package/packages/sdk/dist/token-registry.d.ts +96 -0
  90. package/packages/sdk/dist/token-registry.js +122 -0
  91. package/packages/sdk/dist/utils/encoding.d.ts +19 -0
  92. package/packages/sdk/dist/utils/encoding.js +29 -0
  93. package/packages/sdk/dist/vk-registry.d.ts +100 -0
  94. package/packages/sdk/dist/vk-registry.js +212 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * PSBT builder for UTXOpia non-interactive deposits.
3
+ *
4
+ * Creates a Partially-Signed Bitcoin Transaction with:
5
+ * - Input(s): user's UTXOs (P2TR or P2WPKH)
6
+ * - Output 1: P2TR deposit (commitment-bound Taproot address)
7
+ * - Output 2: OP_RETURN (73 bytes: header || poolTag || ephemeralPub || npk)
8
+ * - Output 3: change back to user (if needed)
9
+ *
10
+ * Uses @scure/btc-signer for PSBT construction.
11
+ */
12
+ import * as btc from "@scure/btc-signer";
13
+ import { hex } from "@scure/base";
14
+ import { DEPOSIT_OP_RETURN_SIZE, createOpReturnScriptFromPayload } from "./taproot";
15
+ // =============================================================================
16
+ // Constants
17
+ // =============================================================================
18
+ /** Dust limit for Bitcoin outputs (satoshis) */
19
+ const DUST_LIMIT = 546;
20
+ /** Estimated vbytes per P2TR key-path input */
21
+ const P2TR_INPUT_VBYTES = 58;
22
+ /** Estimated vbytes per P2WPKH input */
23
+ const P2WPKH_INPUT_VBYTES = 68;
24
+ /** Estimated vbytes for P2TR output */
25
+ const P2TR_OUTPUT_VBYTES = 43;
26
+ /** Estimated vbytes for OP_RETURN output (with compact deposit payload) */
27
+ const OP_RETURN_OUTPUT_VBYTES = 84; // 8 value + 1 script_len + 1 OP_RETURN + 1 push + 73 payload
28
+ /** Transaction overhead (version + locktime + segwit marker + input/output count) */
29
+ const TX_OVERHEAD_VBYTES = 11;
30
+ // =============================================================================
31
+ // Fee Estimation
32
+ // =============================================================================
33
+ /**
34
+ * Estimate the transaction fee for a deposit PSBT.
35
+ */
36
+ export function estimateDepositFee(numInputs, feeRate, inputType = "p2tr", hasChange = true) {
37
+ const inputVbytes = inputType === "p2tr" ? P2TR_INPUT_VBYTES : P2WPKH_INPUT_VBYTES;
38
+ const outputCount = hasChange ? 3 : 2; // deposit + OP_RETURN + optional change
39
+ const vsize = TX_OVERHEAD_VBYTES +
40
+ numInputs * inputVbytes +
41
+ P2TR_OUTPUT_VBYTES + // deposit output
42
+ OP_RETURN_OUTPUT_VBYTES + // OP_RETURN output
43
+ (hasChange ? P2TR_OUTPUT_VBYTES : 0); // change output
44
+ return Math.ceil(vsize * feeRate);
45
+ }
46
+ // =============================================================================
47
+ // PSBT Builder
48
+ // =============================================================================
49
+ /**
50
+ * Build a deposit PSBT with OP_RETURN for non-interactive stealth deposits.
51
+ *
52
+ * The PSBT is unsigned — it must be signed by the user's wallet (e.g. via sats-connect).
53
+ */
54
+ export function buildDepositPsbt(params) {
55
+ const { senderUtxos, depositAddress, depositAmountSats, opReturnPayload, changeAddress, feeRate, network = "testnet", } = params;
56
+ if (senderUtxos.length === 0) {
57
+ throw new Error("No UTXOs provided");
58
+ }
59
+ if (depositAmountSats < DUST_LIMIT) {
60
+ throw new Error(`Deposit amount ${depositAmountSats} is below dust limit ${DUST_LIMIT}`);
61
+ }
62
+ if (opReturnPayload.length !== DEPOSIT_OP_RETURN_SIZE) {
63
+ throw new Error(`OP_RETURN payload must be ${DEPOSIT_OP_RETURN_SIZE} bytes, got ${opReturnPayload.length}`);
64
+ }
65
+ const btcNetwork = network === "mainnet" ? btc.NETWORK : btc.TEST_NETWORK;
66
+ // Calculate total input value
67
+ const totalInput = senderUtxos.reduce((sum, u) => sum + u.value, 0);
68
+ // Detect input type from first UTXO
69
+ const firstScript = hex.decode(senderUtxos[0].scriptPubkeyHex);
70
+ const inputType = firstScript[0] === 0x51 ? "p2tr" : "p2wpkh";
71
+ // Estimate fee with change
72
+ const feeWithChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, true);
73
+ const changeAmount = totalInput - depositAmountSats - feeWithChange;
74
+ // Check if we have enough funds
75
+ const feeWithoutChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, false);
76
+ if (totalInput < depositAmountSats + feeWithoutChange) {
77
+ throw new Error(`Insufficient funds: have ${totalInput} sats, need ${depositAmountSats + feeWithoutChange} sats (including fee)`);
78
+ }
79
+ const hasChange = changeAmount > DUST_LIMIT;
80
+ const actualFee = hasChange ? feeWithChange : totalInput - depositAmountSats;
81
+ // Build the transaction using @scure/btc-signer
82
+ // allowUnknownOutputs is required for the OP_RETURN output script
83
+ const tx = new btc.Transaction({ allowUnknownOutputs: true });
84
+ // Add inputs
85
+ for (const utxo of senderUtxos) {
86
+ const scriptPubkey = hex.decode(utxo.scriptPubkeyHex);
87
+ if (scriptPubkey[0] === 0x51 && scriptPubkey.length === 34) {
88
+ // P2TR input
89
+ tx.addInput({
90
+ txid: utxo.txid,
91
+ index: utxo.vout,
92
+ witnessUtxo: {
93
+ script: scriptPubkey,
94
+ amount: BigInt(utxo.value),
95
+ },
96
+ tapInternalKey: scriptPubkey.slice(2), // x-only pubkey from OP_1 <32 bytes>
97
+ });
98
+ }
99
+ else if (scriptPubkey[0] === 0x00 && scriptPubkey.length === 22) {
100
+ // P2WPKH input
101
+ tx.addInput({
102
+ txid: utxo.txid,
103
+ index: utxo.vout,
104
+ witnessUtxo: {
105
+ script: scriptPubkey,
106
+ amount: BigInt(utxo.value),
107
+ },
108
+ });
109
+ }
110
+ else {
111
+ throw new Error(`Unsupported input script type for UTXO ${utxo.txid}:${utxo.vout}`);
112
+ }
113
+ }
114
+ // Output 1: P2TR deposit
115
+ tx.addOutputAddress(depositAddress, BigInt(depositAmountSats), btcNetwork);
116
+ // Output 2: OP_RETURN with compact deposit payload.
117
+ const opReturnScript = createOpReturnScriptFromPayload(opReturnPayload);
118
+ tx.addOutput({
119
+ script: opReturnScript,
120
+ amount: 0n,
121
+ });
122
+ // Output 3: Change (if above dust)
123
+ if (hasChange) {
124
+ tx.addOutputAddress(changeAddress, BigInt(changeAmount), btcNetwork);
125
+ }
126
+ // Extract PSBT
127
+ const psbtBytes = tx.toPSBT();
128
+ const psbtHex = hex.encode(psbtBytes);
129
+ const psbtBase64 = btoa(String.fromCharCode(...psbtBytes));
130
+ return {
131
+ psbtBase64,
132
+ psbtHex,
133
+ estimatedFee: actualFee,
134
+ totalInput,
135
+ changeAmount: hasChange ? changeAmount : 0,
136
+ };
137
+ }
138
+ /**
139
+ * Fetch UTXOs for an address from mempool.space API.
140
+ */
141
+ export async function fetchUtxos(address, network = "testnet") {
142
+ const baseUrl = network === "mainnet"
143
+ ? "https://mempool.space/api"
144
+ : network === "signet"
145
+ ? "https://mempool.space/signet/api"
146
+ : "https://mempool.space/testnet/api";
147
+ // Fetch UTXOs
148
+ const utxoRes = await fetch(`${baseUrl}/address/${address}/utxo`);
149
+ if (!utxoRes.ok) {
150
+ throw new Error(`Failed to fetch UTXOs: ${utxoRes.status} ${utxoRes.statusText}`);
151
+ }
152
+ const utxos = await utxoRes.json();
153
+ // We need scriptPubkey for each UTXO. Fetch from the tx details.
154
+ // For efficiency, batch unique txids.
155
+ const txidSet = new Set(utxos.map((u) => u.txid));
156
+ const txCache = new Map();
157
+ await Promise.all([...txidSet].map(async (txid) => {
158
+ const txRes = await fetch(`${baseUrl}/tx/${txid}`);
159
+ if (txRes.ok) {
160
+ txCache.set(txid, await txRes.json());
161
+ }
162
+ }));
163
+ return utxos
164
+ .filter((u) => u.status.confirmed) // Only confirmed UTXOs
165
+ .map((u) => {
166
+ const tx = txCache.get(u.txid);
167
+ const output = tx?.vout?.[u.vout];
168
+ const scriptPubkeyHex = output?.scriptpubkey ?? "";
169
+ return {
170
+ txid: u.txid,
171
+ vout: u.vout,
172
+ value: u.value,
173
+ scriptPubkeyHex,
174
+ };
175
+ })
176
+ .filter((u) => u.scriptPubkeyHex.length > 0);
177
+ }
178
+ /**
179
+ * Select UTXOs to cover the target amount + estimated fee.
180
+ * Simple greedy algorithm: sort descending by value, take until covered.
181
+ */
182
+ export function selectUtxos(utxos, targetSats, feeRate) {
183
+ // Sort descending by value
184
+ const sorted = [...utxos].sort((a, b) => b.value - a.value);
185
+ const selected = [];
186
+ let total = 0;
187
+ for (const utxo of sorted) {
188
+ selected.push(utxo);
189
+ total += utxo.value;
190
+ // Estimate fee for current selection
191
+ const fee = estimateDepositFee(selected.length, feeRate);
192
+ if (total >= targetSats + fee) {
193
+ return selected;
194
+ }
195
+ }
196
+ // Not enough funds
197
+ const fee = estimateDepositFee(selected.length, feeRate);
198
+ if (total < targetSats + fee) {
199
+ throw new Error(`Insufficient funds: have ${total} sats, need ${targetSats + fee} sats`);
200
+ }
201
+ return selected;
202
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Selective ZK Disclosure (Phase 4 skeleton)
3
+ *
4
+ * Targeted ZK proofs that reveal a single fact about the prover's notes
5
+ * without handing over a viewing key. Two flavors:
6
+ *
7
+ * 1. Ownership proof — "I own commitment X, amount ≥ Y."
8
+ * Useful for: proof-of-funds to a regulator, dispute resolution,
9
+ * challenge-response with a counterparty.
10
+ *
11
+ * 2. Range-sum proof — "Σ amount_i over a slot range ≤ Z" across a
12
+ * caller-provided set of leaf indices, with a check that I have
13
+ * *committed* to the full set via a Merkle commitment over the set.
14
+ *
15
+ * Status (2026-05-13): types frozen, no live prover. Circuits for both
16
+ * (`ownership.circom`, `range_sum.circom`) need design + trusted setup before
17
+ * runtime can do real work. Until then both generator functions throw.
18
+ *
19
+ * Design notes:
20
+ *
21
+ * Ownership proof. Algorithm sketch:
22
+ * - Inputs (private): spendingPrivScalar, randomIn, valueIn, merkleProof
23
+ * - Inputs (public): commitment, merkleRoot, threshold (Y), token
24
+ * - Constraints:
25
+ * (a) commitment == Poseidon(NPK, token, valueIn) where
26
+ * NPK = Poseidon(MPK, randomIn) and MPK = Poseidon(spendingPub, nk)
27
+ * (b) commitment ∈ tree(merkleRoot)
28
+ * (c) valueIn >= threshold (range check)
29
+ * - The proof produces a *non-spending* witness: no nullifier is emitted,
30
+ * so generating the proof doesn't burn the note.
31
+ *
32
+ * Range-sum proof. Algorithm sketch:
33
+ * - Inputs (private): for each note i: randomIn[i], valueIn[i], merkleProof[i]
34
+ * - Inputs (public): leafIndices[i], merkleRoot, ceiling (Z), token,
35
+ * attestation = Poseidon(leafIndices, viewerNonce) — a binding so the
36
+ * verifier knows the prover committed to *this exact* set of leaves
37
+ * - Constraints:
38
+ * (a) each commitment ∈ tree(merkleRoot)
39
+ * (b) each commitment == Poseidon(NPK_i, token, valueIn[i])
40
+ * (c) Σ valueIn[i] <= ceiling
41
+ * (d) attestation matches
42
+ * - Coverage check (the hard part: proving the prover didn't *omit* notes
43
+ * in the slot range) is enforced by having the verifier independently
44
+ * compute attestation = Poseidon(everyKnownLeaf, viewerNonce) and
45
+ * compare. The verifier learns the leaf set from the public viewing key
46
+ * handed over for that purpose.
47
+ *
48
+ * The viewing-key-coverage dependency means range-sum is most useful when
49
+ * paired with the auditor flow from Phase 1 — a one-shot disclosure that
50
+ * proves a property over the set the auditor already verified.
51
+ */
52
+ import type { ProofData } from "./prover/web";
53
+ export interface OwnershipProofInputs {
54
+ /** Witness data for the note. */
55
+ spendingPrivScalar: bigint;
56
+ nullifyingKey: bigint;
57
+ randomIn: bigint;
58
+ valueIn: bigint;
59
+ pathElements: bigint[];
60
+ pathIndices: number[];
61
+ /** Public commitments the prover is claiming. */
62
+ commitment: bigint;
63
+ merkleRoot: bigint;
64
+ /** Minimum amount the prover asserts the note holds. */
65
+ threshold: bigint;
66
+ tokenId: bigint;
67
+ }
68
+ export interface OwnershipPublicInputs {
69
+ commitment: bigint;
70
+ merkleRoot: bigint;
71
+ threshold: bigint;
72
+ tokenId: bigint;
73
+ }
74
+ /**
75
+ * Generate an ownership ZK proof.
76
+ *
77
+ * Calls into the generic snarkjs prover with the `ownership` circuit.
78
+ * Requires the compiled wasm + zkey under `<circuitBasePath>/ownership/`.
79
+ *
80
+ * Public inputs: [commitment, merkleRoot, threshold, tokenId]
81
+ */
82
+ export declare function generateOwnershipProof(inputs: OwnershipProofInputs): Promise<ProofData>;
83
+ export interface RangeSumProofInputs {
84
+ /** One entry per note in the prover's claimed set. */
85
+ notes: ReadonlyArray<{
86
+ randomIn: bigint;
87
+ valueIn: bigint;
88
+ pathElements: bigint[];
89
+ /** @deprecated No longer used: the circuit derives path bits from leafIndex. */
90
+ pathIndices?: number[];
91
+ commitment: bigint;
92
+ leafIndex: number;
93
+ }>;
94
+ /** Common shared data. */
95
+ spendingPrivScalar: bigint;
96
+ nullifyingKey: bigint;
97
+ merkleRoot: bigint;
98
+ /** Upper bound on the sum the prover asserts. */
99
+ ceiling: bigint;
100
+ tokenId: bigint;
101
+ /** Salt the verifier supplied, binding the proof to this verification round. */
102
+ viewerNonce: bigint;
103
+ /** Poseidon(leafIndices ++ [viewerNonce]) — must match the public input. */
104
+ attestation: bigint;
105
+ }
106
+ export interface RangeSumPublicInputs {
107
+ leafIndices: number[];
108
+ merkleRoot: bigint;
109
+ ceiling: bigint;
110
+ tokenId: bigint;
111
+ /** Poseidon(leafIndices ++ [viewerNonce]) committed to by the prover. */
112
+ attestation: bigint;
113
+ }
114
+ /**
115
+ * How the variant computes `attestation` from `(leafIndices, viewerNonce)`.
116
+ *
117
+ * - "flat" — `Poseidon(leafIndices ++ [viewerNonce])`. Cheap; requires
118
+ * N+1 ≤ 16 because circomlib's Poseidon caps at arity 16.
119
+ * - "chunked" — `Poseidon(Poseidon(leafIndices[0..N/2]),
120
+ * Poseidon(leafIndices[N/2..N]),
121
+ * viewerNonce)`. Needed at N=16 where the flat
122
+ * form would require Poseidon(17). Only one nesting level
123
+ * ships today (N=16); deeper variants would extend the
124
+ * same pattern.
125
+ */
126
+ export type RangeSumAttestationStyle = "flat" | "chunked";
127
+ /**
128
+ * Compiled range-sum variants and the cardinality each handles. Add new
129
+ * entries here when you build a new sibling circuit. The `circuit` field
130
+ * must match the directory name under `circuits/build/`.
131
+ */
132
+ export declare const RANGE_SUM_VARIANTS: readonly [{
133
+ readonly n: 4;
134
+ readonly circuit: "range_sum_4";
135
+ readonly attestation: "flat";
136
+ }, {
137
+ readonly n: 8;
138
+ readonly circuit: "range_sum";
139
+ readonly attestation: "flat";
140
+ }, {
141
+ readonly n: 16;
142
+ readonly circuit: "range_sum_16";
143
+ readonly attestation: "chunked";
144
+ }];
145
+ /** Number of notes accepted by each compiled variant. */
146
+ export declare const RANGE_SUM_SIZES: (8 | 16 | 4)[];
147
+ /**
148
+ * @deprecated Prefer `RANGE_SUM_VARIANTS` / `pickRangeSumVariant`. Retained
149
+ * for back-compat with older callers that assumed a single N.
150
+ */
151
+ export declare const RANGE_SUM_N = 8;
152
+ /** Look up the compiled circuit name for a given note cardinality. */
153
+ export declare function pickRangeSumVariant(n: number): typeof RANGE_SUM_VARIANTS[number];
154
+ /**
155
+ * Compute the range-sum attestation public input the way each compiled
156
+ * circuit expects. SDK + CLI must agree on this exactly or the verifier
157
+ * rejects the proof.
158
+ *
159
+ * - flat: Poseidon(leafIndices ++ [viewerNonce])
160
+ * - chunked: Poseidon(Poseidon(leafIndices[0..N/2]),
161
+ * Poseidon(leafIndices[N/2..N]),
162
+ * viewerNonce)
163
+ *
164
+ * `style` defaults to the cardinality's compiled variant; pass it
165
+ * explicitly if you're computing the value without going through
166
+ * `generateRangeSumProof` (e.g. inside a CLI that lays out witness data
167
+ * up front).
168
+ */
169
+ export declare function computeRangeSumAttestation(leafIndices: ReadonlyArray<number | bigint>, viewerNonce: bigint, style?: RangeSumAttestationStyle): Promise<bigint>;
170
+ /**
171
+ * Generate a range-sum ZK proof. The variant is picked automatically from
172
+ * `inputs.notes.length` — compile + register new variants in
173
+ * `RANGE_SUM_VARIANTS` to extend the supported cardinalities.
174
+ *
175
+ * Calls into the generic snarkjs prover with the matching circuit.
176
+ * Requires the compiled wasm + zkey under
177
+ * `<circuitBasePath>/<variant.circuit>/`.
178
+ *
179
+ * Public inputs: [leafIndices(N), merkleRoot, ceiling, token, attestation]
180
+ */
181
+ export declare function generateRangeSumProof(inputs: RangeSumProofInputs): Promise<ProofData>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Selective ZK Disclosure (Phase 4 skeleton)
3
+ *
4
+ * Targeted ZK proofs that reveal a single fact about the prover's notes
5
+ * without handing over a viewing key. Two flavors:
6
+ *
7
+ * 1. Ownership proof — "I own commitment X, amount ≥ Y."
8
+ * Useful for: proof-of-funds to a regulator, dispute resolution,
9
+ * challenge-response with a counterparty.
10
+ *
11
+ * 2. Range-sum proof — "Σ amount_i over a slot range ≤ Z" across a
12
+ * caller-provided set of leaf indices, with a check that I have
13
+ * *committed* to the full set via a Merkle commitment over the set.
14
+ *
15
+ * Status (2026-05-13): types frozen, no live prover. Circuits for both
16
+ * (`ownership.circom`, `range_sum.circom`) need design + trusted setup before
17
+ * runtime can do real work. Until then both generator functions throw.
18
+ *
19
+ * Design notes:
20
+ *
21
+ * Ownership proof. Algorithm sketch:
22
+ * - Inputs (private): spendingPrivScalar, randomIn, valueIn, merkleProof
23
+ * - Inputs (public): commitment, merkleRoot, threshold (Y), token
24
+ * - Constraints:
25
+ * (a) commitment == Poseidon(NPK, token, valueIn) where
26
+ * NPK = Poseidon(MPK, randomIn) and MPK = Poseidon(spendingPub, nk)
27
+ * (b) commitment ∈ tree(merkleRoot)
28
+ * (c) valueIn >= threshold (range check)
29
+ * - The proof produces a *non-spending* witness: no nullifier is emitted,
30
+ * so generating the proof doesn't burn the note.
31
+ *
32
+ * Range-sum proof. Algorithm sketch:
33
+ * - Inputs (private): for each note i: randomIn[i], valueIn[i], merkleProof[i]
34
+ * - Inputs (public): leafIndices[i], merkleRoot, ceiling (Z), token,
35
+ * attestation = Poseidon(leafIndices, viewerNonce) — a binding so the
36
+ * verifier knows the prover committed to *this exact* set of leaves
37
+ * - Constraints:
38
+ * (a) each commitment ∈ tree(merkleRoot)
39
+ * (b) each commitment == Poseidon(NPK_i, token, valueIn[i])
40
+ * (c) Σ valueIn[i] <= ceiling
41
+ * (d) attestation matches
42
+ * - Coverage check (the hard part: proving the prover didn't *omit* notes
43
+ * in the slot range) is enforced by having the verifier independently
44
+ * compute attestation = Poseidon(everyKnownLeaf, viewerNonce) and
45
+ * compare. The verifier learns the leaf set from the public viewing key
46
+ * handed over for that purpose.
47
+ *
48
+ * The viewing-key-coverage dependency means range-sum is most useful when
49
+ * paired with the auditor flow from Phase 1 — a one-shot disclosure that
50
+ * proves a property over the set the auditor already verified.
51
+ */
52
+ /**
53
+ * Generate an ownership ZK proof.
54
+ *
55
+ * Calls into the generic snarkjs prover with the `ownership` circuit.
56
+ * Requires the compiled wasm + zkey under `<circuitBasePath>/ownership/`.
57
+ *
58
+ * Public inputs: [commitment, merkleRoot, threshold, tokenId]
59
+ */
60
+ export async function generateOwnershipProof(inputs) {
61
+ const { generateGenericGroth16Proof } = await import("./prover/web");
62
+ return generateGenericGroth16Proof("ownership", {
63
+ commitment: inputs.commitment.toString(),
64
+ merkleRoot: inputs.merkleRoot.toString(),
65
+ threshold: inputs.threshold.toString(),
66
+ token: inputs.tokenId.toString(),
67
+ spendingPrivScalar: inputs.spendingPrivScalar.toString(),
68
+ randomIn: inputs.randomIn.toString(),
69
+ valueIn: inputs.valueIn.toString(),
70
+ pathElements: inputs.pathElements.map((e) => e.toString()),
71
+ pathIndices: inputs.pathIndices,
72
+ nullifyingKey: inputs.nullifyingKey.toString(),
73
+ });
74
+ }
75
+ /**
76
+ * Compiled range-sum variants and the cardinality each handles. Add new
77
+ * entries here when you build a new sibling circuit. The `circuit` field
78
+ * must match the directory name under `circuits/build/`.
79
+ */
80
+ export const RANGE_SUM_VARIANTS = [
81
+ { n: 4, circuit: "range_sum_4", attestation: "flat" },
82
+ { n: 8, circuit: "range_sum", attestation: "flat" },
83
+ { n: 16, circuit: "range_sum_16", attestation: "chunked" },
84
+ ];
85
+ /** Number of notes accepted by each compiled variant. */
86
+ export const RANGE_SUM_SIZES = RANGE_SUM_VARIANTS.map((v) => v.n);
87
+ /**
88
+ * @deprecated Prefer `RANGE_SUM_VARIANTS` / `pickRangeSumVariant`. Retained
89
+ * for back-compat with older callers that assumed a single N.
90
+ */
91
+ export const RANGE_SUM_N = 8;
92
+ /** Look up the compiled circuit name for a given note cardinality. */
93
+ export function pickRangeSumVariant(n) {
94
+ const v = RANGE_SUM_VARIANTS.find((x) => x.n === n);
95
+ if (!v) {
96
+ throw new Error(`range_sum has no compiled variant for N=${n}. Compiled variants: ` +
97
+ RANGE_SUM_SIZES.join(", ") +
98
+ ". Pad with zero-value notes or compile a new variant " +
99
+ "(see circuits/scripts/build-aux.sh).");
100
+ }
101
+ return v;
102
+ }
103
+ /**
104
+ * Compute the range-sum attestation public input the way each compiled
105
+ * circuit expects. SDK + CLI must agree on this exactly or the verifier
106
+ * rejects the proof.
107
+ *
108
+ * - flat: Poseidon(leafIndices ++ [viewerNonce])
109
+ * - chunked: Poseidon(Poseidon(leafIndices[0..N/2]),
110
+ * Poseidon(leafIndices[N/2..N]),
111
+ * viewerNonce)
112
+ *
113
+ * `style` defaults to the cardinality's compiled variant; pass it
114
+ * explicitly if you're computing the value without going through
115
+ * `generateRangeSumProof` (e.g. inside a CLI that lays out witness data
116
+ * up front).
117
+ */
118
+ export async function computeRangeSumAttestation(leafIndices, viewerNonce, style) {
119
+ // Only consult the variants registry when the caller didn't pick a style
120
+ // explicitly. The explicit-style path is for tests / parity checks that
121
+ // want to compute the hash without first compiling a variant.
122
+ const resolvedStyle = style ?? pickRangeSumVariant(leafIndices.length).attestation;
123
+ // circomlibjs's Poseidon caps at the same arity (16) as the circom-side
124
+ // hash. Importing the heavy circomlibjs module lazily keeps the SDK
125
+ // bundle slim for callers that never compute an attestation.
126
+ const { buildPoseidon } = await import("circomlibjs");
127
+ const poseidon = (await buildPoseidon());
128
+ const F = poseidon.F;
129
+ const indicesAsField = leafIndices.map((i) => F.e(BigInt(i)));
130
+ if (resolvedStyle === "flat") {
131
+ return F.toObject(poseidon([...indicesAsField, F.e(viewerNonce)]));
132
+ }
133
+ // chunked: split in half, hash each half, then hash the two digests + nonce.
134
+ const half = leafIndices.length / 2;
135
+ if (!Number.isInteger(half)) {
136
+ throw new Error(`chunked attestation requires an even cardinality; got N=${leafIndices.length}`);
137
+ }
138
+ const chunk1 = poseidon(indicesAsField.slice(0, half));
139
+ const chunk2 = poseidon(indicesAsField.slice(half));
140
+ return F.toObject(poseidon([chunk1, chunk2, F.e(viewerNonce)]));
141
+ }
142
+ /**
143
+ * Generate a range-sum ZK proof. The variant is picked automatically from
144
+ * `inputs.notes.length` — compile + register new variants in
145
+ * `RANGE_SUM_VARIANTS` to extend the supported cardinalities.
146
+ *
147
+ * Calls into the generic snarkjs prover with the matching circuit.
148
+ * Requires the compiled wasm + zkey under
149
+ * `<circuitBasePath>/<variant.circuit>/`.
150
+ *
151
+ * Public inputs: [leafIndices(N), merkleRoot, ceiling, token, attestation]
152
+ */
153
+ export async function generateRangeSumProof(inputs) {
154
+ const variant = pickRangeSumVariant(inputs.notes.length);
155
+ const { generateGenericGroth16Proof } = await import("./prover/web");
156
+ return generateGenericGroth16Proof(variant.circuit, {
157
+ leafIndices: inputs.notes.map((n) => n.leafIndex),
158
+ merkleRoot: inputs.merkleRoot.toString(),
159
+ ceiling: inputs.ceiling.toString(),
160
+ token: inputs.tokenId.toString(),
161
+ attestation: inputs.attestation.toString(),
162
+ spendingPrivScalar: inputs.spendingPrivScalar.toString(),
163
+ nullifyingKey: inputs.nullifyingKey.toString(),
164
+ randomIn: inputs.notes.map((n) => n.randomIn.toString()),
165
+ valueIn: inputs.notes.map((n) => n.valueIn.toString()),
166
+ pathElements: inputs.notes.map((n) => n.pathElements.map((e) => e.toString())),
167
+ // pathIndices is derived in-circuit from the public leafIndices (Num2Bits),
168
+ // so it is no longer passed as a witness input.
169
+ viewerNonce: inputs.viewerNonce.toString(),
170
+ commitmentsIn: inputs.notes.map((n) => n.commitment.toString()),
171
+ });
172
+ }