@zbase-protocol/core 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/LICENSE +201 -0
- package/NOTICE +20 -0
- package/README.md +118 -0
- package/dist/account.d.ts +79 -0
- package/dist/account.js +140 -0
- package/dist/experimental.d.ts +27 -0
- package/dist/experimental.js +23 -0
- package/dist/facilitatorClient.d.ts +171 -0
- package/dist/facilitatorClient.js +146 -0
- package/dist/forwardingNotes.d.ts +103 -0
- package/dist/forwardingNotes.js +149 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/merkle.d.ts +48 -0
- package/dist/merkle.js +84 -0
- package/dist/noteScanner.d.ts +206 -0
- package/dist/noteScanner.js +422 -0
- package/dist/notes.d.ts +305 -0
- package/dist/notes.js +547 -0
- package/dist/npk.d.ts +121 -0
- package/dist/npk.js +166 -0
- package/dist/proofs.d.ts +66 -0
- package/dist/proofs.js +45 -0
- package/dist/stealth.d.ts +136 -0
- package/dist/stealth.js +387 -0
- package/dist/swapPlan.d.ts +52 -0
- package/dist/swapPlan.js +104 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +4 -0
- package/dist/viewingKeyHD.d.ts +108 -0
- package/dist/viewingKeyHD.js +176 -0
- package/package.json +71 -0
package/dist/notes.d.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zbase-protocol/core — UTXO note primitives (Shipment A.1 scaffold)
|
|
3
|
+
*
|
|
4
|
+
* v0: in-memory note model + viewing-key encryption for storing notes off-chain
|
|
5
|
+
* and posting their ciphertext alongside the spend's `NoteCommitted` event.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Railgun's note model (https://docs.railgun.org/wiki/learn/privacy-system).
|
|
8
|
+
* The on-chain commitment encoding is intentionally identical to the current
|
|
9
|
+
* single-value commitment in `account.ts` so the same Poseidon helpers stay in
|
|
10
|
+
* use and the Anchor program's `crypto.rs` continues to apply without change:
|
|
11
|
+
*
|
|
12
|
+
* precommitment = Poseidon2(nullifier, secret)
|
|
13
|
+
* commitment = Poseidon3(amount, label, precommitment)
|
|
14
|
+
*
|
|
15
|
+
* Cryptographic envelope chosen for cold-start performance over libsodium.js
|
|
16
|
+
* (no WASM init, no async load) — see /docs/utxo-notes-design.md §4.1.
|
|
17
|
+
*
|
|
18
|
+
* * X25519 key agreement (`@noble/curves/ed25519` → `x25519`)
|
|
19
|
+
* * XChaCha20-Poly1305 (`@noble/ciphers/chacha`)
|
|
20
|
+
* * 2-byte view-tag (Railgun-style fast scan filter)
|
|
21
|
+
*
|
|
22
|
+
* Status: scaffold. Do NOT use the encryption helpers in production until
|
|
23
|
+
* (a) the bytes layout has been pinned by a wallet review and
|
|
24
|
+
* (b) the trusted-setup ceremony for `note_spend.circom` has completed.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Plaintext note. Lives off-chain (in the wallet, in the facilitator's
|
|
28
|
+
* per-user cache, or as the decrypted payload of a `NoteCommitted` log).
|
|
29
|
+
*
|
|
30
|
+
* v1 RECIPE (audit C3): the on-chain commitment recipe was migrated to match
|
|
31
|
+
* the frozen `note_spend.circom`:
|
|
32
|
+
*
|
|
33
|
+
* NPK = Poseidon2(spendingPK, viewingPKBlind)
|
|
34
|
+
* commitment = Poseidon3(amount, NPK, secret)
|
|
35
|
+
*
|
|
36
|
+
* The previous v0 recipe `Poseidon3(amount, label, Poseidon2(nullifier,secret))`
|
|
37
|
+
* did NOT match the circuit, so any UTXO proof the SDK built would have failed
|
|
38
|
+
* verification. `label` is no longer part of the commitment (it now binds
|
|
39
|
+
* privately through the NPK + the ASP-membership witness), but is retained on
|
|
40
|
+
* the note because the circuit still proves ASP membership of `label`.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ AUDITOR-SCOPED GAP: this aligns the commitment HASH STRUCTURE with the
|
|
43
|
+
* circuit. How `spendingPK` / `viewingPKBlind` are DERIVED from the recipient's
|
|
44
|
+
* viewing key (the Railgun NPK key-derivation, and exactly how the ASP label is
|
|
45
|
+
* folded into that derivation off-chain) is NOT yet implemented in the SDK and
|
|
46
|
+
* must be designed + reviewed in the external circuit audit before the UTXO pool
|
|
47
|
+
* goes live. Until then these are caller-supplied fields.
|
|
48
|
+
*/
|
|
49
|
+
export interface Note {
|
|
50
|
+
/** USDC atomic units (6 decimal places). 0 marks a dummy / zero-note. */
|
|
51
|
+
amount: bigint;
|
|
52
|
+
/** ASP label inherited from the originating deposit. Proven for ASP
|
|
53
|
+
* membership; NOT part of the commitment in v1. */
|
|
54
|
+
label: bigint;
|
|
55
|
+
/** Recipient spending public key (field element) — NPK input. */
|
|
56
|
+
spendingPK: bigint;
|
|
57
|
+
/** Per-note blinded recipient viewing public key (field element) — NPK input. */
|
|
58
|
+
viewingPKBlind: bigint;
|
|
59
|
+
/** Per-note nullifier (random field element). */
|
|
60
|
+
nullifier: bigint;
|
|
61
|
+
/** Per-note secret (random field element). */
|
|
62
|
+
secret: bigint;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Viewing keypair. The private key gives read access to all notes addressed to
|
|
66
|
+
* this owner; it cannot move funds (which require the per-note nullifier+secret).
|
|
67
|
+
*/
|
|
68
|
+
export interface ViewingKeyPair {
|
|
69
|
+
privateKey: Uint8Array;
|
|
70
|
+
publicKey: Uint8Array;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Encrypted note as it would appear on-chain inside a `NoteCommitted` log:
|
|
74
|
+
*
|
|
75
|
+
* bytes layout:
|
|
76
|
+
* [0..32) ephemeral X25519 public key
|
|
77
|
+
* [32..34) 2-byte view-tag (firstTwoBytes(keccak(shared)))
|
|
78
|
+
* [34..58) XChaCha20 nonce (24 bytes)
|
|
79
|
+
* [58..) AEAD ciphertext (plaintext_len + 16-byte Poly1305 tag)
|
|
80
|
+
*
|
|
81
|
+
* Total per-note overhead ≈ 58 + ciphertext_len. The plaintext is ~120-150
|
|
82
|
+
* bytes after JSON serialization, putting per-note calldata cost at ~3K gas
|
|
83
|
+
* (well under the ~250K Groth16 verification cost).
|
|
84
|
+
*/
|
|
85
|
+
export type EncryptedNote = Uint8Array;
|
|
86
|
+
/**
|
|
87
|
+
* Create a fresh note with random nullifier + secret.
|
|
88
|
+
*
|
|
89
|
+
* `amount` + `label` come from a deposit or a parent note being split. The NPK
|
|
90
|
+
* inputs (`spendingPK`, `viewingPKBlind`) identify the RECIPIENT and bind the
|
|
91
|
+
* label privately — they default to 0 here for the self-spend / scaffold case,
|
|
92
|
+
* but a real transfer MUST pass the recipient's derived NPK inputs.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ Deriving spendingPK/viewingPKBlind from a recipient's viewing key is the
|
|
95
|
+
* auditor-scoped gap (see the Note doc). Until that lands, callers either pass
|
|
96
|
+
* 0 (change-back-to-self placeholder) or supply explicit values.
|
|
97
|
+
*/
|
|
98
|
+
export declare function createNote(amount: bigint, label: bigint, npk?: {
|
|
99
|
+
spendingPK: bigint;
|
|
100
|
+
viewingPKBlind: bigint;
|
|
101
|
+
}): Note;
|
|
102
|
+
/**
|
|
103
|
+
* Create a zero-value dummy note. Used as a placeholder input/output to fill
|
|
104
|
+
* the fixed N=2/M=2 slots of the v0 spend circuit when the spender does not
|
|
105
|
+
* actually need 2 inputs or 2 outputs.
|
|
106
|
+
*
|
|
107
|
+
* A dummy carries amount=0 and label=0; the circuit's `inIsDummy[i]` flag
|
|
108
|
+
* must be set to 1 for input dummies. (Output dummies are valid as-is because
|
|
109
|
+
* a 0-amount output commitment hashes deterministically and inserts harmlessly
|
|
110
|
+
* into the state tree — though wallets should treat amount=0 commitments as
|
|
111
|
+
* unspendable noise.)
|
|
112
|
+
*/
|
|
113
|
+
export declare function createDummyNote(): Note;
|
|
114
|
+
/**
|
|
115
|
+
* `NPK = Poseidon2(spendingPK, viewingPKBlind)`. The note-public-key the
|
|
116
|
+
* circuit's `NpkBuilder` template computes (note_spend.circom NpkBuilder).
|
|
117
|
+
*/
|
|
118
|
+
export declare function npkOf(note: Pick<Note, "spendingPK" | "viewingPKBlind">): bigint;
|
|
119
|
+
/**
|
|
120
|
+
* v1 UTXO commitment (audit C3): `commitment = Poseidon3(amount, NPK, secret)`,
|
|
121
|
+
* `NPK = Poseidon2(spendingPK, viewingPKBlind)`.
|
|
122
|
+
*
|
|
123
|
+
* This now matches `CommitmentHasher` in `note_spend.circom` EXACTLY (Poseidon3
|
|
124
|
+
* over `[amount, npk, secret]`). The prior v0 recipe
|
|
125
|
+
* `Poseidon3(amount, label, Poseidon2(nullifier, secret))` did NOT match the
|
|
126
|
+
* circuit and would have made every UTXO proof unverifiable.
|
|
127
|
+
*
|
|
128
|
+
* NOTE: this is the UTXO-pool recipe. The legacy single-value pool keeps its own
|
|
129
|
+
* (correct, deployed) recipe in `account.ts::computeCommitment` — do not unify
|
|
130
|
+
* them; they back two different pools.
|
|
131
|
+
*/
|
|
132
|
+
export declare function commitmentOf(note: Note): bigint;
|
|
133
|
+
/**
|
|
134
|
+
* `nullifierHash = Poseidon1(nullifier)`. Imported from account.ts. Repeated
|
|
135
|
+
* here only for convenient note-centric API.
|
|
136
|
+
*/
|
|
137
|
+
export declare function nullifierHashOf(note: Note): bigint;
|
|
138
|
+
export declare class NoteValueError extends Error {
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Circuit range bound (audit F11): `note_spend.circom` range-checks every output
|
|
142
|
+
* amount and the withdrawn amount to 64 bits (`Num2Bits(64)`). Amounts at or
|
|
143
|
+
* above 2^64 would fail proof generation with an opaque error, or — if a caller
|
|
144
|
+
* skipped the SDK — risk field-wraparound creating value. We validate here so
|
|
145
|
+
* bad amounts are rejected up front with a clear message.
|
|
146
|
+
*/
|
|
147
|
+
export declare const MAX_NOTE_AMOUNT: bigint;
|
|
148
|
+
/**
|
|
149
|
+
* Split a single note into two new notes that conserve the parent value and
|
|
150
|
+
* inherit its label. The two outputs are returned in the order
|
|
151
|
+
* `[primary, change]`; pass them straight into a 2-output spend.
|
|
152
|
+
*/
|
|
153
|
+
export declare function splitNote(parent: Note, primaryAmount: bigint): [Note, Note];
|
|
154
|
+
/**
|
|
155
|
+
* Merge two notes (with matching label) into one. Returns the merged note.
|
|
156
|
+
* The caller must spend the two inputs and mint this one output via a spend.
|
|
157
|
+
*/
|
|
158
|
+
export declare function mergeNotes(a: Note, b: Note): Note;
|
|
159
|
+
/**
|
|
160
|
+
* Plan the inputs/outputs for paying `payAmount` out of an array of available
|
|
161
|
+
* notes. Returns the (≤2) input notes and (≤2) output notes for a v0 spend.
|
|
162
|
+
*
|
|
163
|
+
* Strategy:
|
|
164
|
+
* - Pick the smallest single note ≥ payAmount if possible (minimises change).
|
|
165
|
+
* - Otherwise merge the two largest notes — error if their sum is still
|
|
166
|
+
* insufficient (caller must perform multiple spends; defer to a future
|
|
167
|
+
* "PaymentPlanner" helper).
|
|
168
|
+
*
|
|
169
|
+
* This is a v0 heuristic; production wallets should use a coin-selection
|
|
170
|
+
* algorithm tuned for anonymity-set size (e.g., randomised LIFO).
|
|
171
|
+
*/
|
|
172
|
+
export declare function planSpend(available: readonly Note[], payAmount: bigint): {
|
|
173
|
+
inputs: [Note, Note];
|
|
174
|
+
outputAmounts: [bigint, bigint];
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Plan one dust-consolidation step (Phase 1C operational fix).
|
|
178
|
+
*
|
|
179
|
+
* Fragmented wallets leak: every spend that has to merge two inputs reveals
|
|
180
|
+
* (via the conservation law) that the spender held exactly those two note
|
|
181
|
+
* values, and a wallet forced into many small spends produces a recognisable
|
|
182
|
+
* on-chain cadence. Keeping the note count low ahead of time means real
|
|
183
|
+
* payments can take the single-input path, which is the quietest shape.
|
|
184
|
+
*
|
|
185
|
+
* Given the available notes and a target count, returns the inputs/outputs
|
|
186
|
+
* for ONE 2-in/1-out merge spend (the v0 circuit is fixed at 2×2, so a merge
|
|
187
|
+
* consumes two notes and mints one; the second output slot carries 0):
|
|
188
|
+
*
|
|
189
|
+
* - Only notes with amount > 0 count toward the threshold (zero-amount
|
|
190
|
+
* commitments are unspendable noise by convention — see createDummyNote).
|
|
191
|
+
* - Returns null when the spendable count is already ≤ threshold, or when
|
|
192
|
+
* no two spendable notes share a label (merging across labels would mix
|
|
193
|
+
* ASP attestations — same rule as mergeNotes).
|
|
194
|
+
* - Within the most-fragmented label group, the two SMALLEST notes merge
|
|
195
|
+
* first: dust is the priority, and small notes are the ones whose values
|
|
196
|
+
* are most identifiable in conservation-law analysis.
|
|
197
|
+
*
|
|
198
|
+
* Repeat-callable: apply the plan (spend the two inputs, keep the merged
|
|
199
|
+
* output), then call again with the updated set. Each round reduces the
|
|
200
|
+
* spendable count by exactly one, so looping until null terminates.
|
|
201
|
+
*/
|
|
202
|
+
export declare function consolidateNotes(available: readonly Note[], threshold?: number): {
|
|
203
|
+
inputs: [Note, Note];
|
|
204
|
+
outputAmounts: [bigint, bigint];
|
|
205
|
+
} | null;
|
|
206
|
+
/**
|
|
207
|
+
* Serialize a note to a compact JSON-friendly form. BigInts are encoded as
|
|
208
|
+
* decimal strings; the round-trip is exact. Used for localStorage today; will
|
|
209
|
+
* back the facilitator's encrypted note cache later.
|
|
210
|
+
*/
|
|
211
|
+
export declare function serializeNote(n: Note): string;
|
|
212
|
+
export declare function deserializeNote(s: string): Note;
|
|
213
|
+
/**
|
|
214
|
+
* Generate a fresh X25519 viewing keypair. Private key is sampled from
|
|
215
|
+
* `randomBytes` (= crypto.getRandomValues underneath). Curve constraints
|
|
216
|
+
* (clamping) are handled by `x25519`'s scalar multiplication.
|
|
217
|
+
*/
|
|
218
|
+
export declare function generateViewingKey(): ViewingKeyPair;
|
|
219
|
+
/**
|
|
220
|
+
* Encrypt a note to a recipient's viewing public key.
|
|
221
|
+
*
|
|
222
|
+
* Returns the on-chain blob (see `EncryptedNote` layout above).
|
|
223
|
+
*
|
|
224
|
+
* Caveats:
|
|
225
|
+
* - The output is non-deterministic (ephemeral key + random nonce).
|
|
226
|
+
* - Includes a 2-byte view-tag to make recipient scanning O(N) trial-decrypt
|
|
227
|
+
* instead of O(N) full AEAD-decrypt.
|
|
228
|
+
* - Does NOT bind the ciphertext to any on-chain commitment. If you want
|
|
229
|
+
* AAD binding (preventing a malicious relayer from swapping ciphertexts
|
|
230
|
+
* between commitments) pass the commitment hash as `aad`.
|
|
231
|
+
*/
|
|
232
|
+
export declare function encryptNote(note: Note, recipientPublicKey: Uint8Array, aad?: Uint8Array): EncryptedNote;
|
|
233
|
+
/**
|
|
234
|
+
* Attempt to decrypt a note using a viewing private key. Returns `null` on
|
|
235
|
+
* view-tag mismatch (fast path: 99.998% of foreign notes reject here) or on
|
|
236
|
+
* AEAD failure (cryptographically authenticated decryption).
|
|
237
|
+
*
|
|
238
|
+
* `aad` must match what the sender supplied at encryption time (commonly the
|
|
239
|
+
* commitment hash as bytes).
|
|
240
|
+
*/
|
|
241
|
+
export declare function decryptNote(encrypted: EncryptedNote, viewingPrivateKey: Uint8Array, aad?: Uint8Array): Note | null;
|
|
242
|
+
/**
|
|
243
|
+
* Scan an array of (commitment, encryptedNote) pairs and decrypt all notes
|
|
244
|
+
* addressed to the viewing key. Convenience helper for the wallet/facilitator.
|
|
245
|
+
*
|
|
246
|
+
* `aadFor` allows AAD binding to the commitment (default: no AAD).
|
|
247
|
+
*/
|
|
248
|
+
export declare function scanNotes(entries: ReadonlyArray<{
|
|
249
|
+
commitment: bigint;
|
|
250
|
+
encrypted: EncryptedNote;
|
|
251
|
+
}>, viewingPrivateKey: Uint8Array, aadFor?: (commitment: bigint) => Uint8Array | undefined): Array<{
|
|
252
|
+
commitment: bigint;
|
|
253
|
+
note: Note;
|
|
254
|
+
}>;
|
|
255
|
+
/**
|
|
256
|
+
* Build the AAD that binds a ciphertext to its on-chain commitment.
|
|
257
|
+
*
|
|
258
|
+
* Must match the value the UTXOPool contract verifies in `transfer()`:
|
|
259
|
+
*
|
|
260
|
+
* aad = keccak256(abi.encode(outputCommitment)) (Solidity)
|
|
261
|
+
* = keccak256(uint256ToBE32(commitment)) (TypeScript, here)
|
|
262
|
+
*
|
|
263
|
+
* Returns the 32-byte keccak256 digest. Passing the bare commitment bytes
|
|
264
|
+
* instead would still bind to the commitment but would NOT match the contract
|
|
265
|
+
* — the on-chain hash is mandatory for the Phase 1B Fix 3 ciphertext-shuffle
|
|
266
|
+
* defense to engage.
|
|
267
|
+
*/
|
|
268
|
+
export declare function commitmentAAD(commitment: bigint): Uint8Array;
|
|
269
|
+
/**
|
|
270
|
+
* Convenience wrapper around `encryptNote` for the transfer use case.
|
|
271
|
+
* Auto-derives the AAD from the commitment so callers can't accidentally
|
|
272
|
+
* forget to bind. Equivalent to:
|
|
273
|
+
*
|
|
274
|
+
* encryptNote(note, recipientPub, commitmentAAD(commitment))
|
|
275
|
+
*
|
|
276
|
+
* Use this everywhere the SDK builds `CommitmentCiphertext` for `transfer()`.
|
|
277
|
+
*/
|
|
278
|
+
export declare function encryptNoteForTransfer(note: Note, recipientViewingPubKey: Uint8Array, commitment: bigint): EncryptedNote;
|
|
279
|
+
/**
|
|
280
|
+
* Decrypt a transfer-bound ciphertext, requiring the AAD to match the given
|
|
281
|
+
* commitment. Returns `null` on:
|
|
282
|
+
*
|
|
283
|
+
* - view-tag mismatch (fast path — not this key's note),
|
|
284
|
+
* - AEAD authentication failure (wrong key, corrupted blob),
|
|
285
|
+
* - AAD mismatch (relayer attempted to shuffle ciphertexts between
|
|
286
|
+
* commitments — Phase 1B Fix 3).
|
|
287
|
+
*
|
|
288
|
+
* Callers should ALWAYS use this in the scanner, never the bare `decryptNote`,
|
|
289
|
+
* for `Transferred`-event ciphertexts. The bare form is reserved for legacy
|
|
290
|
+
* `NoteCommitted` payloads (Shipment A.1 v0) which predate AAD binding.
|
|
291
|
+
*/
|
|
292
|
+
export declare function decryptNoteWithAAD(encrypted: EncryptedNote, viewingPrivateKey: Uint8Array, expectedCommitment: bigint): Note | null;
|
|
293
|
+
/** Wire-format version for packed transfer ciphertexts. */
|
|
294
|
+
export declare const CIPHERTEXT_WIRE_VERSION: 1;
|
|
295
|
+
/**
|
|
296
|
+
* Frame an EncryptedNote for the on-chain `CommitmentCiphertext.ciphertext`
|
|
297
|
+
* field. Returns a 0x-hex string ready to pass as Solidity `bytes`.
|
|
298
|
+
*/
|
|
299
|
+
export declare function packCiphertext(encrypted: EncryptedNote): `0x${string}`;
|
|
300
|
+
/**
|
|
301
|
+
* Inverse of packCiphertext: recover the EncryptedNote blob from the on-chain
|
|
302
|
+
* `bytes`. Returns null on malformed / wrong-version / length-mismatch input
|
|
303
|
+
* (a scanner should skip, not throw, on a payload it can't parse).
|
|
304
|
+
*/
|
|
305
|
+
export declare function unpackCiphertext(packedHex: string): EncryptedNote | null;
|