@openzeppelin/compact-contracts 0.4.0-alpha.6 → 0.5.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 (43) hide show
  1. package/access/AccessControl.compact +1 -1
  2. package/access/Ownable.compact +1 -1
  3. package/access/ShieldedAccessControl.compact +30 -26
  4. package/access/ZOwnablePK.compact +1 -1
  5. package/crypto/Ecdh.compact +153 -0
  6. package/crypto/EcdhMask.compact +103 -70
  7. package/crypto/Ecdsa.compact +1 -1
  8. package/crypto/Eip712.compact +1 -1
  9. package/crypto/ElGamal.compact +1 -1
  10. package/multisig/ForwarderPrivate.compact +1 -1
  11. package/multisig/ForwarderShielded.compact +1 -1
  12. package/multisig/ForwarderUnshielded.compact +1 -1
  13. package/multisig/ProposalManager.compact +1 -1
  14. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  15. package/multisig/ShieldedTreasury.compact +1 -1
  16. package/multisig/ShieldedTreasuryStateless.compact +1 -1
  17. package/multisig/Signer.compact +1 -1
  18. package/multisig/UnshieldedTreasury.compact +1 -1
  19. package/multisig/examples/ForwarderPrivateExample.compact +1 -1
  20. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  21. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +1 -1
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +1 -1
  25. package/multisig/presets/ShieldedMultiSigV2.compact +1 -1
  26. package/package.json +1 -1
  27. package/security/Allowlist.compact +1 -1
  28. package/security/Blocklist.compact +1 -1
  29. package/security/Initializable.compact +1 -1
  30. package/security/Pausable.compact +1 -1
  31. package/token/ConfidentialFungibleToken.compact +1 -1
  32. package/token/ConfidentialNoteFungibleToken.compact +741 -0
  33. package/token/FungibleToken.compact +1 -1
  34. package/token/MultiToken.compact +1 -1
  35. package/token/NativeShieldedToken.compact +1 -1
  36. package/token/NativeShieldedTokenCore.compact +1 -1
  37. package/token/NativeShieldedTokenFamily.compact +1 -1
  38. package/token/NonFungibleToken.compact +1 -1
  39. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  40. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  41. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  42. package/utils/EvmAbi.compact +1 -1
  43. package/utils/Utils.compact +1 -1
@@ -0,0 +1,741 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (token/ConfidentialNoteFungibleToken.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module ConfidentialNoteFungibleToken<#depth>
8
+ * @description
9
+ * A note-based confidential token: amounts, sender, and recipient are hidden
10
+ * from the public ledger.
11
+ *
12
+ * @dev Notation:
13
+ * - `H(...)`: `persistentHash` (SHA-256), a `Bytes<32>` digest.
14
+ * - `Hf(...)`: `H` reduced to a `Field` via `degradeToTransient`.
15
+ * - NOTE: a UTxO `(value, nonce)`. A balance is the sum of one's unspent notes.
16
+ * - `pk = Hf(domain, salt, sk)`: the owner identity, `salt` being the
17
+ * deployment's `_instanceSalt`.
18
+ * - `cm = H(domain, value, nonce, pk)`: the commitment stored in the tree.
19
+ * - `nf = H(domain, nonce)`: the nullifier that marks a note spent.
20
+ * - `tag = H(domain, nonce)`: the issued tag that reserves a nonce at mint.
21
+ *
22
+ * @dev Derivations:
23
+ * `H` hashes the named preimage struct in Compact's encoding, not a byte
24
+ * concatenation. Do not reimplement it: `derivePk`, `commitOf`, `nullifierOf`
25
+ * and `issuedTagOf` are exported pure circuits, and the compatibility suite
26
+ * pins their vectors. `derivePk` takes the deployment's `_instanceSalt` as
27
+ * `salt`. A wrong derivation yields notes that cannot be found and identities
28
+ * that cannot spend. No pure circuit covers `_mint`'s nonce derivation:
29
+ * the caller receives the note, so nothing off-chain recomputes it.
30
+ *
31
+ * @dev Parameterization:
32
+ * `#depth` is the commitment tree's depth, fixed at import time
33
+ * (`import "./ConfidentialNoteFungibleToken"<32> prefix Core_;`). It sets
34
+ * capacity at `2^depth` notes and the length of every Merkle path, so it is
35
+ * part of the deployed wire format: two instantiations at different depths are
36
+ * different contracts and share no notes.
37
+ *
38
+ * @dev Compatibility:
39
+ * The wire format is the domain tags (`pk`'s included), the preimage struct
40
+ * layouts, and `depth`. Changing any of them orphans every existing note. The
41
+ * compatibility suite pins all three.
42
+ *
43
+ * @notice Privacy:
44
+ * - Public, per tx:
45
+ * - The entry point, so the operation type.
46
+ * - One `nf` per note spent.
47
+ * - One `tag` and one opaque leaf digest per note created.
48
+ * - The Merkle root the spend proves against.
49
+ * - The transcript shape, which does not vary with amount or recipient.
50
+ * - Public, as state:
51
+ * - The init flag, the instance salt, the commitment tree, the nullifier
52
+ * set, the issued-nonce set.
53
+ * - No balances, no accounts, no supply.
54
+ * - Private:
55
+ * - Amounts.
56
+ * - Sender and recipient `pk`s, even under `sk` reuse: `pk` is
57
+ * domain-tagged and salted by `_instanceSalt`, so no other contract's
58
+ * identity for the same `sk` derives it.
59
+ * - Nonces and secret keys.
60
+ * - Which leaf a spend consumes, within the leaves present at the
61
+ * disclosed root. A stale root narrows that set to older leaves, and a
62
+ * rarely refreshed one fingerprints the wallet.
63
+ * - The link between a mint and its spend, absent the nonce.
64
+ * - Counterparty: whoever knows a note's nonce derives its `tag` and `nf`, so
65
+ * sees WHEN it is minted and spent, not by whom or for how much.
66
+ * - Auditability: no view key. Disclosure is per note, by sharing its nonce.
67
+ * Supply is not tracked; a composer adds it.
68
+ *
69
+ * @notice Security:
70
+ * - Nonces are spend-critical: `nf` depends on the nonce alone, so anyone who
71
+ * knows a nonce derives the same nullifier and can race the owner for the
72
+ * single spend. This is also what lets a composer offer escrow-free
73
+ * clawback: two spends race for one `nf`, first to land wins.
74
+ * - Freshness: `wit_ConfidentialNoteNonceRandomness` MUST return a fresh
75
+ * secret seed per call. Outputs bind their context (the spent `nf`, or the
76
+ * recipient for
77
+ * `_mint`), and `_mintNote` rejects an already-issued nonce, so a repeated
78
+ * seed fails loudly instead of producing a dead note.
79
+ * - Ownership: a spend proves `cm` binds `_spenderPk()`; no secret, no spend.
80
+ * - No authorization on `_` circuits: `_mint`, `_mintNote`, `_transfer`,
81
+ * `_burn`, `_consumeNote` are ungated. Re-exporting one from a deployed
82
+ * contract is a permissionless mint or spend. The composer gates them.
83
+ *
84
+ * @notice Concurrency:
85
+ * - Tree and set inserts commute; two calls collide only where they pin the
86
+ * same key.
87
+ * - Spends pin `_nullifiers.member(nf)`: only two spends of the SAME note
88
+ * conflict.
89
+ * - Mints pin `_issuedNonces.member(tag)`: only a duplicate nonce conflicts.
90
+ * - `checkRoot` on the historic tree pins "root in history", so a proof built
91
+ * against a stale root still lands after concurrent inserts.
92
+ * - Net: calls on distinct notes in one block all land; see the concurrency
93
+ * suite for the exhaustive pairwise matrix.
94
+ *
95
+ * @notice Scope:
96
+ * Note machinery only: no roles. The one setup step is `initialize(salt)`,
97
+ * called from the composer's constructor; every mint and spend requires it.
98
+ * `transfer` and `burn` are self-gated by `wit_ConfidentialNoteSK` and the
99
+ * caller's randomness witness.
100
+ * Created notes return to the caller as a private result and reach recipients
101
+ * out of band.
102
+ *
103
+ * @notice Client-side duties:
104
+ * - Identity: `pk = derivePk(sk, _instanceSalt)` is the owner's identity on
105
+ * this deployment only. Salted per deployment and domain-separated from
106
+ * both the `ZswapCoinPublicKey` and the public modules' `computeAccountId`,
107
+ * it links no identities when `sk` is reused across contracts. Read the
108
+ * salt from the ledger, or take it from the deployer for a constructor-time
109
+ * mint. Derive `sk` from the wallet seed under a module-specific domain so
110
+ * one seed backup covers it, and share `pk` out of band.
111
+ * - Implement the four witnesses: `wit_ConfidentialNoteSK`,
112
+ * `wit_ConfidentialNoteInputNote`, `wit_ConfidentialNotePath`,
113
+ * `wit_ConfidentialNoteNonceRandomness`.
114
+ * - Keep every note and its leaf position. There is no memo and no viewing
115
+ * key, so a chain rescan finds nothing: the seed restores identity, not
116
+ * balance. Notes need their own backup.
117
+ * - Deliver notes to recipients over a channel you provide; the contract has
118
+ * no inbox. A recipient should confirm the leaf digest is in the tree
119
+ * before accepting.
120
+ *
121
+ * @notice Scalability:
122
+ * - Capacity: `2^depth` notes per deployment; every output, including a
123
+ * zero-value change note, takes a leaf.
124
+ * - State growth: `_nullifiers` and `_issuedNonces` gain one entry per note,
125
+ * and the historic tree keeps every past root. Nothing is pruned.
126
+ * - One input per spend and no note merging: a balance spread over N notes
127
+ * needs N spends, and every transfer adds a change note.
128
+ * - Throughput follows the concurrency rules above: calls on distinct notes
129
+ * commute.
130
+ * - Wallets need no chain scanning, since notes arrive out of band, but must
131
+ * track Merkle paths, so an indexer over all leaves is required. Fetch the
132
+ * path right before proving: the root it yields is public.
133
+ *
134
+ * @dev Performance:
135
+ * - Proving cost is per call and does not grow with the tree: the path is
136
+ * always `depth` deep, and each level costs a fixed number of rows.
137
+ * - Cost is dominated by SHA-256. Every output pays the nonce, `cm` and `tag`
138
+ * hashes; a spend adds the input `cm`, `nf` and the path on top.
139
+ * - `transfer` is the largest circuit. Per-circuit `k` and rows are on each
140
+ * `@constraints` line.
141
+ *
142
+ * @dev Composition:
143
+ * Value creation has no default gate. `_mint` / `_mintNote` are ungated, and
144
+ * the composer decides who may create value. The `_` circuits accept
145
+ * caller-built notes so a composer can run its own nonce policy; `_mint` uses
146
+ * the core default. A constructor cannot return, so a constructor-time mint
147
+ * is `_mintNote` with a deployer-built note unless the same constructor
148
+ * records the note on chain encrypted to its owner.
149
+ */
150
+ module ConfidentialNoteFungibleToken<#depth> {
151
+ import CompactStandardLibrary;
152
+
153
+ /**
154
+ * @description A unit of value. Ownership is not a field: a note belongs to
155
+ * whoever's `pk` was bound into its commitment (see `commitOf`).
156
+ * @type {Uint<128>} value - The amount the note carries.
157
+ * @type {Field} nonce - The commitment's hiding entropy and the sole
158
+ * nullifier preimage, so it is spend-critical.
159
+ */
160
+ export struct Note {
161
+ value: Uint<128>;
162
+ nonce: Field;
163
+ }
164
+
165
+ /**
166
+ * @description Domain-separated preimage of an owner identity.
167
+ * @type {Bytes<32>} domain - The pk domain tag.
168
+ * @type {Bytes<32>} salt - The deployment salt.
169
+ * @type {Bytes<32>} sk - The owner's spend secret.
170
+ */
171
+ struct PkPreimage {
172
+ domain: Bytes<32>;
173
+ salt: Bytes<32>;
174
+ sk: Bytes<32>;
175
+ }
176
+
177
+ /**
178
+ * @description Domain-separated preimage of a note commitment.
179
+ * @type {Bytes<32>} domain - The commitment domain tag.
180
+ * @type {Uint<128>} value - The note's value.
181
+ * @type {Field} nonce - The note's nonce.
182
+ * @type {Field} pk - The owner's spend identity.
183
+ */
184
+ struct CommitPreimage {
185
+ domain: Bytes<32>;
186
+ value: Uint<128>;
187
+ nonce: Field;
188
+ pk: Field;
189
+ }
190
+
191
+ /**
192
+ * @description Preimage of a nullifier or an issued tag. The owner is
193
+ * deliberately absent; `domain` alone keeps the two from colliding.
194
+ * @type {Bytes<32>} domain - The nullifier or issued-tag domain tag.
195
+ * @type {Field} nonce - The note's nonce.
196
+ */
197
+ struct NullifierPreimage {
198
+ domain: Bytes<32>;
199
+ nonce: Field;
200
+ }
201
+
202
+ /**
203
+ * @description Domain-separated preimage of a derived output nonce.
204
+ * @type {Bytes<32>} seed - The caller's per-invocation randomness.
205
+ * @type {Bytes<32>} domain - The nonce domain tag.
206
+ * @type {Bytes<32>} slot - The per-output tag.
207
+ * @type {Field} binder - What the output is tied to.
208
+ */
209
+ struct NoncePreimage {
210
+ seed: Bytes<32>;
211
+ domain: Bytes<32>;
212
+ slot: Bytes<32>;
213
+ binder: Field;
214
+ }
215
+
216
+ /**
217
+ * @description Initialization flag, tracked per-module so that two modules
218
+ * sharing a transitive dependency do not share one flag.
219
+ */
220
+ export sealed ledger _isInitialized: Boolean;
221
+
222
+ /**
223
+ * @description The salt every `pk` on this deployment is derived under. Set
224
+ * once by `initialize`.
225
+ * @type {Bytes<32>} _instanceSalt - The deployment salt.
226
+ */
227
+ export sealed ledger _instanceSalt: Bytes<32>;
228
+
229
+ /**
230
+ * @description Append-only tree of note commitments: what value exists. It
231
+ * does not prevent double-spends; that is `_nullifiers`' job.
232
+ * @dev Historic, not plain:
233
+ * `checkRoot` accepts any past root, so a proof survives concurrent inserts.
234
+ * @type {HistoricMerkleTree<depth, Bytes<32>>} _commitments - The commitment
235
+ * tree, holding up to `2^depth` leaves.
236
+ */
237
+ export ledger _commitments: HistoricMerkleTree<depth, Bytes<32>>;
238
+
239
+ /**
240
+ * @description Published nullifiers: what has been spent. Unlinkable to any
241
+ * commitment, owner, or value, so to an observer who does not hold the note
242
+ * the set reveals only a spend count. See the header's Privacy notice for
243
+ * what a nonce-holder sees.
244
+ * @type {Set<Bytes<32>>} _nullifiers - The spent-note nullifier set.
245
+ */
246
+ export ledger _nullifiers: Set<Bytes<32>>;
247
+
248
+ /**
249
+ * @description One tag per note ever committed. Separate from `_nullifiers`
250
+ * so that set keeps meaning "spent". See `_mintNote`.
251
+ * @type {Set<Bytes<32>>} _issuedNonces - The issued-tag set.
252
+ */
253
+ export ledger _issuedNonces: Set<Bytes<32>>;
254
+
255
+ /**
256
+ * @witness wit_ConfidentialNoteSK
257
+ * @description Returns the caller's spend secret, from which the circuits
258
+ * derive the identity `pk = derivePk(sk, _instanceSalt)` that owns notes.
259
+ *
260
+ * @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random
261
+ * value.
262
+ */
263
+ witness wit_ConfidentialNoteSK(): Bytes<32>;
264
+
265
+ /**
266
+ * @witness wit_ConfidentialNoteInputNote
267
+ * @description Returns the note the next spend consumes. The wallet chooses
268
+ * it; the circuit proves it exists and is unspent.
269
+ *
270
+ * @returns {Note} inputNote - The note to consume.
271
+ */
272
+ witness wit_ConfidentialNoteInputNote(): Note;
273
+
274
+ /**
275
+ * @witness wit_ConfidentialNotePath
276
+ * @description Returns the Merkle path proving `cm` is a leaf of the
277
+ * commitment tree. The path stays witness, so only membership is proved.
278
+ *
279
+ * @param {Bytes<32>} cm - The input note's commitment.
280
+ * @returns {MerkleTreePath<depth, Bytes<32>>} path - The `depth`-entry path
281
+ * from `cm` to a root the tree recognizes, current or historical.
282
+ */
283
+ witness wit_ConfidentialNotePath(cm: Bytes<32>): MerkleTreePath<depth, Bytes<32>>;
284
+
285
+ /**
286
+ * @witness wit_ConfidentialNoteNonceRandomness
287
+ * @description Returns the seed the core derives its default output nonces
288
+ * from.
289
+ *
290
+ * @dev Freshness is mandatory:
291
+ * MUST be fresh and secret per call. A reused seed re-derives an identical
292
+ * note; `_mintNote` rejects the duplicate rather than committing it.
293
+ *
294
+ * @returns {Bytes<32>} seed - A 32-byte cryptographically secure random
295
+ * value.
296
+ */
297
+ witness wit_ConfidentialNoteNonceRandomness(): Bytes<32>;
298
+
299
+ /**
300
+ * @description Sets the salt every `pk` on this deployment is derived
301
+ * under. Call it from the constructor; every mint and spend requires it.
302
+ *
303
+ * @dev Sealed:
304
+ * `_instanceSalt` is writable only during construction.
305
+ *
306
+ * Requirements:
307
+ *
308
+ * - Contract is not initialized.
309
+ * - `instanceSalt` is not zero.
310
+ *
311
+ * @param {Bytes<32>} instanceSalt - A strong random value unique to this
312
+ * deployment.
313
+ * @return {[]} - Empty tuple.
314
+ */
315
+ export circuit initialize(instanceSalt: Bytes<32>): [] {
316
+ assertNotInitialized();
317
+ assert(instanceSalt != default<Bytes<32>>,
318
+ "ConfidentialNoteFungibleToken: instance salt must not be zero");
319
+ _isInitialized = true;
320
+ _instanceSalt = disclose(instanceSalt);
321
+ }
322
+
323
+ /**
324
+ * @description Asserts that the contract has been initialized.
325
+ *
326
+ * @return {[]} - Empty tuple.
327
+ */
328
+ circuit assertInitialized(): [] {
329
+ assert(_isInitialized, "ConfidentialNoteFungibleToken: contract not initialized");
330
+ }
331
+
332
+ /**
333
+ * @description Asserts that the contract has not been initialized.
334
+ *
335
+ * @return {[]} - Empty tuple.
336
+ */
337
+ circuit assertNotInitialized(): [] {
338
+ assert(!_isInitialized, "ConfidentialNoteFungibleToken: contract already initialized");
339
+ }
340
+
341
+ /**
342
+ * @description Mints a note of `value` to `recipientPk` with a core-derived
343
+ * nonce and returns it.
344
+ *
345
+ * @constraints k=15, rows=17319
346
+ *
347
+ * @notice Hidden amount and recipient:
348
+ * The entry point makes the mint public; the amount and the recipient stay
349
+ * hidden. An account-based token cannot offer this.
350
+ *
351
+ * @warning No authorization:
352
+ * The composer gates who may call this.
353
+ *
354
+ * @warning Constructor use:
355
+ * A constructor cannot return, so a note minted from one is unreachable
356
+ * unless the same constructor records it on chain. Otherwise build the
357
+ * note off-chain and call `_mintNote`.
358
+ *
359
+ * @dev Recipient-bound nonce:
360
+ * With no note consumed, the nonce binds `recipientPk` in place of a
361
+ * nullifier. A composer wanting its own nonce policy calls `_mintNote`.
362
+ *
363
+ * @param {Field} recipientPk - The identity receiving the note.
364
+ * @param {Uint<128>} value - The amount to create.
365
+ * @return {Note} - The new note, returned as a local private result.
366
+ */
367
+ export circuit _mint(recipientPk: Field, value: Uint<128>): Note {
368
+ const note = Note {
369
+ value: value,
370
+ nonce: freshNonce(pad(32, "CNFT:mint"), recipientPk)
371
+ };
372
+ _mintNote(note, recipientPk);
373
+ return disclose(note);
374
+ }
375
+
376
+ /**
377
+ * @description Derives one output nonce from the caller's randomness witness,
378
+ * the output's slot, and what the output is bound to.
379
+ *
380
+ * @dev Slot and binder:
381
+ * Outputs of one invocation share a single witness call, so `slot` is what
382
+ * keeps them apart. `binder` scopes a repeated seed to one spend or one
383
+ * recipient.
384
+ *
385
+ * @param {Bytes<32>} slot - The per-output domain tag.
386
+ * @param {Field} binder - What the output is tied to: the consumed note's
387
+ * nullifier for a spend, the recipient for a mint.
388
+ * @return {Field} - The output note's nonce.
389
+ */
390
+ circuit freshNonce(slot: Bytes<32>, binder: Field): Field {
391
+ return degradeToTransient(persistentHash<NoncePreimage>(NoncePreimage {
392
+ seed: wit_ConfidentialNoteNonceRandomness(),
393
+ domain: pad(32, "CNFT:nonce:core"),
394
+ slot: slot,
395
+ binder: binder
396
+ }));
397
+ }
398
+
399
+ /**
400
+ * @description Commits a caller-built note to `ownerPk`. No value
401
+ * accounting: the caller owns conservation.
402
+ *
403
+ * @constraints k=14, rows=10926
404
+ *
405
+ * @notice Nonce reservation:
406
+ * A duplicate nonce fails the transaction instead of committing a note born
407
+ * dead, so every note in the tree carries a nonce no other note has.
408
+ *
409
+ * @dev Why not `_nullifiers`:
410
+ * Inserting `nf` would spend the note at birth. A member-only check would
411
+ * disclose `nf` at mint, linking mint to spend, and would miss two unspent
412
+ * duplicates.
413
+ *
414
+ * @dev Weak seed:
415
+ * The tag is as guessable as `nf`. A guessable seed exposes at mint what
416
+ * `nf` exposes at spend, in return for catching the RNG break at once.
417
+ *
418
+ * @dev Why a set:
419
+ * A counter or the tree index would pin one shared value and serialize
420
+ * mints. `member` pins one key.
421
+ *
422
+ * @dev Constructor mints:
423
+ * The deployer builds the note and keeps its nonce, so this is the
424
+ * constructor path when the composer does not record it on chain.
425
+ *
426
+ * @warning Predictable nonces:
427
+ * The reservation is first come, first served and keyed on the nonce alone. A
428
+ * caller-chosen nonce that others can guess, such as a counter or tx context,
429
+ * lets anyone reserve it first and fail the intended mint. Derive nonces from
430
+ * secret randomness, and on `nonce already issued` derive a fresh one rather
431
+ * than retrying.
432
+ *
433
+ * @warning No authorization:
434
+ * The composer gates who may call this.
435
+ *
436
+ * Requirements:
437
+ *
438
+ * - Contract is initialized.
439
+ * - The note's nonce has not been issued before.
440
+ *
441
+ * @param {Note} note - The note to commit.
442
+ * @param {Field} ownerPk - The identity that will own it.
443
+ * @return {[]} - Empty tuple.
444
+ */
445
+ export circuit _mintNote(note: Note, ownerPk: Field): [] {
446
+ assertInitialized();
447
+ // Reserve before committing, so a duplicate fails with nothing written.
448
+ const tag = issuedTagOf(note);
449
+ assert(!_issuedNonces.member(disclose(tag)),
450
+ "ConfidentialNoteFungibleToken: nonce already issued");
451
+ _issuedNonces.insert(disclose(tag));
452
+ _commitments.insert(disclose(commitOf(note, ownerPk)));
453
+ }
454
+
455
+ /**
456
+ * @description `tag = H(domain, nonce)`. Exported so off-chain viewers can
457
+ * check whether a nonce is already taken.
458
+ *
459
+ * @dev Domain separation:
460
+ * Only the domain distinguishes this from `nullifierOf`, so neither set can
461
+ * be read as the other.
462
+ *
463
+ * @param {Note} note - The note to tag.
464
+ * @return {Bytes<32>} - The tag published in `_issuedNonces` when the note is
465
+ * committed.
466
+ */
467
+ export pure circuit issuedTagOf(note: Note): Bytes<32> {
468
+ return persistentHash<NullifierPreimage>(NullifierPreimage {
469
+ domain: pad(32, "CNFT:issued"),
470
+ nonce: note.nonce
471
+ });
472
+ }
473
+
474
+ /**
475
+ * @description `cm = H(domain, value, nonce, pk)`. Exported so off-chain
476
+ * viewers can recompute commitments for notes they recover.
477
+ *
478
+ * @notice Unforgeable ownership:
479
+ * Binding the owner is what enforces it: a non-owner's `pk` hashes to a leaf
480
+ * that is not in the tree, so no membership proof exists.
481
+ *
482
+ * @param {Note} note - The note to commit to.
483
+ * @param {Field} pk - The identity that owns the note.
484
+ * @return {Bytes<32>} - The hiding commitment stored in `_commitments`.
485
+ */
486
+ export pure circuit commitOf(note: Note, pk: Field): Bytes<32> {
487
+ return persistentHash<CommitPreimage>(CommitPreimage {
488
+ domain: pad(32, "CNFT:commitment"),
489
+ value: note.value,
490
+ nonce: note.nonce,
491
+ pk: pk
492
+ });
493
+ }
494
+
495
+ /**
496
+ * @description Burns `value` from the caller's input note: spends the note
497
+ * and re-issues only the change, so `value` leaves circulation.
498
+ *
499
+ * @constraints k=16, rows=37889
500
+ *
501
+ * @notice No supply record:
502
+ * The core writes no supply, so nothing on the ledger records that value was
503
+ * destroyed. The amount and the burner stay hidden.
504
+ *
505
+ * Requirements:
506
+ *
507
+ * - The input note is committed in the tree and unspent.
508
+ * - The caller owns the input note.
509
+ * - `value <= input.value`.
510
+ *
511
+ * @param {Uint<128>} value - The amount to burn.
512
+ * @return {Note} - The caller's change note, returned as a local private
513
+ * result.
514
+ */
515
+ export circuit burn(value: Uint<128>): Note {
516
+ const pk = _spenderPk();
517
+ const input = _inputNote();
518
+ assert(input.value >= value, "ConfidentialNoteFungibleToken: insufficient note value");
519
+
520
+ // Peeked, not consumed: `_burn` is what publishes the nullifier.
521
+ const spendId = degradeToTransient(nullifierOf(input));
522
+ const changeNote = Note {
523
+ value: (input.value - value) as Uint<128>,
524
+ nonce: freshNonce(pad(32, "CNFT:change"), spendId)
525
+ };
526
+ _burn(pk, value, changeNote);
527
+ return disclose(changeNote);
528
+ }
529
+
530
+ /**
531
+ * @description The caller's spend identity on this deployment,
532
+ * `derivePk(wit_ConfidentialNoteSK(), _instanceSalt)`. Exposed so a
533
+ * composer authorizes spends the way the core does.
534
+ *
535
+ * @constraints k=13, rows=4195
536
+ *
537
+ * @return {Field} - The caller's spend identity.
538
+ */
539
+ export circuit _spenderPk(): Field {
540
+ return derivePk(wit_ConfidentialNoteSK(), _instanceSalt);
541
+ }
542
+
543
+ /**
544
+ * @description `pk = Hf(domain, salt, sk)`. Exported so wallets and auditors
545
+ * derive identities the way the circuits do.
546
+ *
547
+ * @dev Instance salt:
548
+ * The circuits pass `_instanceSalt`, so one `sk` maps to an unrelated `pk`
549
+ * on every deployment with its own salt, and none equals the public
550
+ * modules' `computeAccountId(sk)`.
551
+ *
552
+ * @param {Bytes<32>} sk - The owner's spend secret.
553
+ * @param {Bytes<32>} salt - The deployment's `_instanceSalt`.
554
+ * @return {Field} - The spend identity that owns notes.
555
+ */
556
+ export pure circuit derivePk(sk: Bytes<32>, salt: Bytes<32>): Field {
557
+ return degradeToTransient(persistentHash<PkPreimage>(PkPreimage {
558
+ domain: pad(32, "CNFT:pk"),
559
+ salt: salt,
560
+ sk: sk
561
+ }));
562
+ }
563
+
564
+ /**
565
+ * @description Peeks at the note the next spend consumes, so a composer can
566
+ * size the change before spending. Reads the witness `_consumeNote` reads.
567
+ *
568
+ * @dev Peek is untrusted:
569
+ * A mismatch with what the composer then passes to `_transfer` / `_burn` is
570
+ * caught by their conservation asserts, which re-read this witness.
571
+ *
572
+ * @return {Note} - The note the next spend will consume.
573
+ */
574
+ export circuit _inputNote(): Note {
575
+ return wit_ConfidentialNoteInputNote();
576
+ }
577
+
578
+ /**
579
+ * @description UNGATED building block: consumes the input note owned by
580
+ * `spenderPk` and re-issues only `changeNote`, so `value` leaves circulation.
581
+ *
582
+ * @constraints k=15, rows=23151
583
+ *
584
+ * @warning No authorization:
585
+ * The composer gates who may call this.
586
+ *
587
+ * Requirements:
588
+ *
589
+ * - The input note is committed in the tree and unspent.
590
+ * - The input note's commitment binds `spenderPk`.
591
+ * - `input.value == value + changeNote.value`.
592
+ *
593
+ * @param {Field} spenderPk - The identity that owns the input note.
594
+ * @param {Uint<128>} value - The amount destroyed.
595
+ * @param {Note} changeNote - The spender's change note.
596
+ * @return {[]} - Empty tuple.
597
+ */
598
+ export circuit _burn(spenderPk: Field, value: Uint<128>, changeNote: Note): [] {
599
+ const input = _consumeNote(spenderPk);
600
+ assert(input.value == value + changeNote.value,
601
+ "ConfidentialNoteFungibleToken: burn does not conserve value");
602
+ _mintNote(changeNote, spenderPk);
603
+ }
604
+
605
+ /**
606
+ * @description The heart of the module: consumes the witness-supplied note
607
+ * owned by `ownerPk` and publishes its nullifier.
608
+ *
609
+ * @constraints k=14, rows=12124
610
+ *
611
+ * @notice Where it is used:
612
+ * By `_transfer` / `_burn`, and by custom spend paths such as clawback,
613
+ * where `ownerPk` is the target and the authorization is the caller's own.
614
+ *
615
+ * @warning No authorization:
616
+ * The composer gates who may call this.
617
+ *
618
+ * Requirements:
619
+ *
620
+ * - Contract is initialized.
621
+ * - The input note is committed in the tree, under `ownerPk`, and unspent.
622
+ * - The witnessed path's leaf is the input note's commitment.
623
+ * - The path's root is one the tree recognizes, current or historical.
624
+ *
625
+ * @param {Field} ownerPk - The identity the input note is committed to.
626
+ * @return {Note} - The consumed note, for the caller's value accounting.
627
+ */
628
+ export circuit _consumeNote(ownerPk: Field): Note {
629
+ assertInitialized();
630
+ const input = wit_ConfidentialNoteInputNote();
631
+
632
+ // The root is public, but the path stays witness, so disclosing the root
633
+ // does not say which leaf was consumed, only that it predates the root.
634
+ const cm = commitOf(input, ownerPk);
635
+ const path = wit_ConfidentialNotePath(cm);
636
+ const root = disclose(merkleTreePathRoot<depth, Bytes<32>>(path));
637
+ assert(_commitments.checkRoot(root),
638
+ "ConfidentialNoteFungibleToken: input root not recognized");
639
+ assert(cm == path.leaf,
640
+ "ConfidentialNoteFungibleToken: path does not match input commitment");
641
+
642
+ const nf = nullifierOf(input);
643
+ assert(!_nullifiers.member(disclose(nf)),
644
+ "ConfidentialNoteFungibleToken: note already spent");
645
+ _nullifiers.insert(disclose(nf));
646
+
647
+ return input;
648
+ }
649
+
650
+ /**
651
+ * @description `nf = H(domain, nonce)` — derivable by anyone who knows the
652
+ * nonce. Exported so off-chain viewers can watch a note's consumption.
653
+ *
654
+ * @dev Owner-free preimage:
655
+ * Omitting the owner secret is what makes clawback escrow-free: two parties
656
+ * knowing the nonce race for one spend.
657
+ *
658
+ * @param {Note} note - The note to nullify.
659
+ * @return {Bytes<32>} - The nullifier published in `_nullifiers` when the
660
+ * note is spent.
661
+ */
662
+ export pure circuit nullifierOf(note: Note): Bytes<32> {
663
+ return persistentHash<NullifierPreimage>(NullifierPreimage {
664
+ domain: pad(32, "CNFT:nullifier"),
665
+ nonce: note.nonce
666
+ });
667
+ }
668
+
669
+ /**
670
+ * @description Spends the caller's input note and creates a recipient note
671
+ * of `value` plus a change note back to the caller, conserving value.
672
+ *
673
+ * @constraints k=16, rows=55048
674
+ *
675
+ * @notice Local private result:
676
+ * Both notes return to the caller, who hands the recipient note over out of
677
+ * band and keeps the change.
678
+ *
679
+ * Requirements:
680
+ *
681
+ * - The input note is committed in the tree and unspent.
682
+ * - The caller owns the input note (its commitment binds `_spenderPk()`).
683
+ * - `value <= input.value`.
684
+ *
685
+ * @param {Field} recipientPk - The identity receiving the output note.
686
+ * @param {Uint<128>} value - The amount to send.
687
+ * @return {[Note, Note]} - The recipient's note and the caller's change note.
688
+ */
689
+ export circuit transfer(recipientPk: Field, value: Uint<128>): [Note, Note] {
690
+ const pk = _spenderPk();
691
+ const input = _inputNote();
692
+ assert(input.value >= value, "ConfidentialNoteFungibleToken: insufficient note value");
693
+
694
+ // Peeked, not consumed: `_transfer` is what publishes the nullifier.
695
+ const spendId = degradeToTransient(nullifierOf(input));
696
+ const outNote = Note {
697
+ value: value,
698
+ nonce: freshNonce(pad(32, "CNFT:out"), spendId)
699
+ };
700
+ const changeNote = Note {
701
+ value: (input.value - value) as Uint<128>,
702
+ nonce: freshNonce(pad(32, "CNFT:change"), spendId)
703
+ };
704
+ _transfer(pk, recipientPk, outNote, changeNote);
705
+ return [disclose(outNote), disclose(changeNote)];
706
+ }
707
+
708
+ /**
709
+ * @description Consumes the input note owned by `spenderPk` and commits
710
+ * `outNote` to `recipientPk` plus `changeNote` back, conserving value.
711
+ *
712
+ * @constraints k=16, rows=33905
713
+ *
714
+ * @notice Caller-built outputs:
715
+ * Taking both notes as parameters lets a composer run its own emission
716
+ * policy while the core still enforces conservation. Output nonces must be
717
+ * unpredictable; see `_mintNote`.
718
+ *
719
+ * @warning No authorization:
720
+ * The composer gates who may call this.
721
+ *
722
+ * Requirements:
723
+ *
724
+ * - The input note is committed in the tree and unspent.
725
+ * - The input note's commitment binds `spenderPk`.
726
+ * - `input.value == outNote.value + changeNote.value`.
727
+ *
728
+ * @param {Field} spenderPk - The identity that owns the input note.
729
+ * @param {Field} recipientPk - The identity receiving `outNote`.
730
+ * @param {Note} outNote - The recipient's output note.
731
+ * @param {Note} changeNote - The spender's change note.
732
+ * @return {[]} - Empty tuple.
733
+ */
734
+ export circuit _transfer(spenderPk: Field, recipientPk: Field, outNote: Note, changeNote: Note): [] {
735
+ const input = _consumeNote(spenderPk);
736
+ assert(input.value == outNote.value + changeNote.value,
737
+ "ConfidentialNoteFungibleToken: transfer does not conserve value");
738
+ _mintNote(outNote, recipientPk);
739
+ _mintNote(changeNote, spenderPk);
740
+ }
741
+ }