Skip to content

Commit 3a951b0

Browse files
committed
refactor(token): scope witness names to the module
Compact merges witnesses by name across imported modules. A contract composing this module with any other that also declares wit_SecretKey gets one witness serving both, so a single implementation would answer for two unrelated secrets and the composer has no way to keep them apart. The clash is silent: it type-checks and compiles. Prefixing each name with the module removes the collision by construction. Witness names are compile-time identifiers, so the measured rows are unchanged. The wit_Path error string reaches assertions in the functional suite and is borrowed as a fixture by the shared rejection test-util, so both move with it. Refs: #743 (comment)
1 parent c0650f2 commit 3a951b0

5 files changed

Lines changed: 47 additions & 41 deletions

File tree

contracts/src/token/ConfidentialNoteFungibleToken.compact

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ pragma language_version >= 0.23.0;
6363
* knows a nonce derives the same nullifier and can race the owner for the
6464
* single spend. This is also what lets a composer offer escrow-free
6565
* clawback: two spends race for one `nf`, first to land wins.
66-
* - Freshness: `wit_NonceRandomness` MUST return a fresh secret seed per call.
67-
* Outputs also bind their context (the spent `nf`, or the recipient for
66+
* - Freshness: `wit_ConfidentialNoteNonceRandomness` MUST return a fresh
67+
* secret seed per call. Outputs bind their context (the spent `nf`, or the
68+
* recipient for
6869
* `_mint`), and `_mintNote` rejects an already-issued nonce, so a repeated
6970
* seed fails loudly instead of producing a dead note.
70-
* - Ownership: a spend proves `cm` binds `Hf(wit_SecretKey())`; no secret, no
71-
* spend.
71+
* - Ownership: a spend proves `cm` binds `Hf(wit_ConfidentialNoteSK())`; no
72+
* secret, no spend.
7273
* - No authorization on `_` circuits: `_mint`, `_mintNote`, `_transfer`,
7374
* `_burn`, `_consumeNote` are ungated. Re-exporting one from a deployed
7475
* contract is a permissionless mint or spend. The composer gates them.
@@ -88,16 +89,17 @@ pragma language_version >= 0.23.0;
8889
*
8990
* @notice Scope:
9091
* Note machinery only: no roles, no initialization. `transfer` and `burn` are
91-
* self-gated by `wit_SecretKey` and the caller's randomness witness. Created
92-
* notes return to the caller as a private result and reach recipients out of
93-
* band.
92+
* self-gated by `wit_ConfidentialNoteSK` and the caller's randomness witness.
93+
* Created notes return to the caller as a private result and reach recipients
94+
* out of band.
9495
*
9596
* @notice Client-side duties:
9697
* - Identity: `pk = Hf(sk)` is this token's address, unrelated to the Zswap
9798
* coin key. Derive `sk` from the wallet seed under a module-specific domain
9899
* so one seed backup covers it, and share `pk` out of band.
99-
* - Implement the four witnesses: `wit_SecretKey`, `wit_InputNote`,
100-
* `wit_Path`, `wit_NonceRandomness`.
100+
* - Implement the four witnesses: `wit_ConfidentialNoteSK`,
101+
* `wit_ConfidentialNoteInputNote`, `wit_ConfidentialNotePath`,
102+
* `wit_ConfidentialNoteNonceRandomness`.
101103
* - Keep every note and its leaf position. There is no memo and no viewing
102104
* key, so a chain rescan finds nothing: the seed restores identity, not
103105
* balance. Notes need their own backup.
@@ -210,37 +212,37 @@ module ConfidentialNoteFungibleToken<#depth> {
210212
export ledger _issuedNonces: Set<Bytes<32>>;
211213

212214
/**
213-
* @witness wit_SecretKey
215+
* @witness wit_ConfidentialNoteSK
214216
* @description Returns the caller's spend secret, from which the circuits
215217
* derive the identity `pk = Hf(sk)` that owns notes.
216218
*
217219
* @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random
218220
* value.
219221
*/
220-
witness wit_SecretKey(): Bytes<32>;
222+
witness wit_ConfidentialNoteSK(): Bytes<32>;
221223

222224
/**
223-
* @witness wit_InputNote
225+
* @witness wit_ConfidentialNoteInputNote
224226
* @description Returns the note the next spend consumes. The wallet chooses
225227
* it; the circuit proves it exists and is unspent.
226228
*
227229
* @returns {Note} inputNote - The note to consume.
228230
*/
229-
witness wit_InputNote(): Note;
231+
witness wit_ConfidentialNoteInputNote(): Note;
230232

231233
/**
232-
* @witness wit_Path
234+
* @witness wit_ConfidentialNotePath
233235
* @description Returns the Merkle path proving `cm` is a leaf of the
234236
* commitment tree. The path stays witness, so only membership is proved.
235237
*
236238
* @param {Bytes<32>} cm - The input note's commitment.
237239
* @returns {MerkleTreePath<depth, Bytes<32>>} path - The `depth`-entry path
238240
* from `cm` to a root the tree recognizes, current or historical.
239241
*/
240-
witness wit_Path(cm: Bytes<32>): MerkleTreePath<depth, Bytes<32>>;
242+
witness wit_ConfidentialNotePath(cm: Bytes<32>): MerkleTreePath<depth, Bytes<32>>;
241243

242244
/**
243-
* @witness wit_NonceRandomness
245+
* @witness wit_ConfidentialNoteNonceRandomness
244246
* @description Returns the seed the core derives its default output nonces
245247
* from.
246248
*
@@ -251,7 +253,7 @@ module ConfidentialNoteFungibleToken<#depth> {
251253
* @returns {Bytes<32>} seed - A 32-byte cryptographically secure random
252254
* value.
253255
*/
254-
witness wit_NonceRandomness(): Bytes<32>;
256+
witness wit_ConfidentialNoteNonceRandomness(): Bytes<32>;
255257

256258
/**
257259
* @description Mints a note of `value` to `recipientPk` with a core-derived
@@ -299,7 +301,7 @@ module ConfidentialNoteFungibleToken<#depth> {
299301
*/
300302
circuit freshNonce(slot: Bytes<32>, binder: Field): Field {
301303
return degradeToTransient(persistentHash<NoncePreimage>(NoncePreimage {
302-
seed: wit_NonceRandomness(),
304+
seed: wit_ConfidentialNoteNonceRandomness(),
303305
domain: pad(32, "OZ:note:nonce:core"),
304306
slot: slot,
305307
binder: binder
@@ -425,13 +427,14 @@ module ConfidentialNoteFungibleToken<#depth> {
425427
}
426428

427429
/**
428-
* @description The caller's spend identity, `Hf(wit_SecretKey())`. Exposed
429-
* so a composer authorizes spends the way the core does.
430+
* @description The caller's spend identity,
431+
* `Hf(wit_ConfidentialNoteSK())`. Exposed so a composer authorizes spends
432+
* the way the core does.
430433
*
431434
* @return {Field} - The caller's spend identity.
432435
*/
433436
export circuit _spenderPk(): Field {
434-
return derivePk(wit_SecretKey());
437+
return derivePk(wit_ConfidentialNoteSK());
435438
}
436439

437440
/**
@@ -456,7 +459,7 @@ module ConfidentialNoteFungibleToken<#depth> {
456459
* @return {Note} - The note the next spend will consume.
457460
*/
458461
export circuit _inputNote(): Note {
459-
return wit_InputNote();
462+
return wit_ConfidentialNoteInputNote();
460463
}
461464

462465
/**
@@ -509,12 +512,12 @@ module ConfidentialNoteFungibleToken<#depth> {
509512
* @return {Note} - The consumed note, for the caller's value accounting.
510513
*/
511514
export circuit _consumeNote(ownerPk: Field): Note {
512-
const input = wit_InputNote();
515+
const input = wit_ConfidentialNoteInputNote();
513516

514517
// The root is public, but the path stays witness, so disclosing the root
515518
// does not say which leaf was consumed.
516519
const cm = commitOf(input, ownerPk);
517-
const path = wit_Path(cm);
520+
const path = wit_ConfidentialNotePath(cm);
518521
const root = disclose(merkleTreePathRoot<depth, Bytes<32>>(path));
519522
assert(_commitments.checkRoot(root),
520523
"ConfidentialNoteFungibleToken: input root not recognized");
@@ -561,7 +564,8 @@ module ConfidentialNoteFungibleToken<#depth> {
561564
* Requirements:
562565
*
563566
* - The input note is committed in the tree and unspent.
564-
* - The caller owns the input note (its commitment binds `Hf(wit_SecretKey())`).
567+
* - The caller owns the input note (its commitment binds
568+
* `Hf(wit_ConfidentialNoteSK())`).
565569
* - `value <= input.value`.
566570
*
567571
* @param {Field} recipientPk - The identity receiving the output note.

contracts/src/token/test/ConfidentialNoteFungibleToken.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ describe('ConfidentialNoteFungibleToken: burn', () => {
373373

374374
await expectRejection(
375375
token.burn(30n),
376-
'wit_Path: commitment not found in tree',
376+
'wit_ConfidentialNotePath: commitment not found in tree',
377377
);
378378
});
379379
});
@@ -530,14 +530,14 @@ describe('ConfidentialNoteFungibleToken: _consumeNote', () => {
530530

531531
await expectRejection(
532532
token._consumeNote(ALICE),
533-
'wit_Path: commitment not found in tree',
533+
'wit_ConfidentialNotePath: commitment not found in tree',
534534
);
535535
});
536536

537537
it('should not consume a note under an owner pk it was not committed to', async () => {
538538
await expectRejection(
539539
token._consumeNote(BOB),
540-
'wit_Path: commitment not found in tree',
540+
'wit_ConfidentialNotePath: commitment not found in tree',
541541
);
542542
});
543543

@@ -725,7 +725,7 @@ describe('ConfidentialNoteFungibleToken: transfer', () => {
725725

726726
await expectRejection(
727727
token.transfer(BOB, 30n),
728-
'wit_Path: commitment not found in tree',
728+
'wit_ConfidentialNotePath: commitment not found in tree',
729729
);
730730
});
731731

@@ -736,7 +736,7 @@ describe('ConfidentialNoteFungibleToken: transfer', () => {
736736

737737
await expectRejection(
738738
token.transfer(CAROL, 30n),
739-
'wit_Path: commitment not found in tree',
739+
'wit_ConfidentialNotePath: commitment not found in tree',
740740
);
741741
expect(await isSpent(input)).toBe(false);
742742
});

contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenSimulator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class ConfidentialNoteFungibleTokenSimulator extends ConfidentialNoteFung
8888
return this.circuits.impure.burn(value);
8989
}
9090

91-
/** The caller's spend identity, `Hf(wit_SecretKey())`. */
91+
/** The caller's spend identity, `Hf(wit_ConfidentialNoteSK())`. */
9292
public _spenderPk(): Promise<bigint> {
9393
return this.circuits.impure._spenderPk();
9494
}

contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenWitnesses.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,42 @@ export const ConfidentialNoteFungibleTokenPrivateState = {
5151
};
5252

5353
export interface IConfidentialNoteFungibleTokenWitnesses<P> {
54-
wit_SecretKey(context: WitnessContext<Ledger, P>): [P, Uint8Array];
55-
wit_InputNote(context: WitnessContext<Ledger, P>): [P, Note];
56-
wit_Path(
54+
wit_ConfidentialNoteSK(context: WitnessContext<Ledger, P>): [P, Uint8Array];
55+
wit_ConfidentialNoteInputNote(context: WitnessContext<Ledger, P>): [P, Note];
56+
wit_ConfidentialNotePath(
5757
context: WitnessContext<Ledger, P>,
5858
cm: Uint8Array,
5959
): [P, MerkleTreePath<Uint8Array>];
60-
wit_NonceRandomness(context: WitnessContext<Ledger, P>): [P, Uint8Array];
60+
wit_ConfidentialNoteNonceRandomness(
61+
context: WitnessContext<Ledger, P>,
62+
): [P, Uint8Array];
6163
}
6264

6365
export const ConfidentialNoteFungibleTokenWitnesses = (
6466
wallet: NoteWallet,
6567
): IConfidentialNoteFungibleTokenWitnesses<ConfidentialNoteFungibleTokenPrivateState> => ({
66-
wit_SecretKey(context) {
68+
wit_ConfidentialNoteSK(context) {
6769
return [context.privateState, wallet.secretKey];
6870
},
69-
wit_InputNote(context) {
71+
wit_ConfidentialNoteInputNote(context) {
7072
return [context.privateState, wallet.inputNote];
7173
},
7274
// The circuit passes the input commitment; the wallet answers with its
7375
// Merkle path, read here from the live commitment tree.
74-
wit_Path(context, cm) {
76+
wit_ConfidentialNotePath(context, cm) {
7577
const planted = wallet.pathOverride;
7678
if (planted !== undefined) {
7779
return [context.privateState, planted];
7880
}
7981
const path = context.ledger.Core__commitments.findPathForLeaf(cm);
8082
if (path === undefined) {
81-
throw new Error('wit_Path: commitment not found in tree');
83+
throw new Error('wit_ConfidentialNotePath: commitment not found in tree');
8284
}
8385
return [context.privateState, path];
8486
},
8587
// Fresh and secret per call, as the module requires; a fixed seed is only
8688
// honored when a spec explicitly plants one.
87-
wit_NonceRandomness(context) {
89+
wit_ConfidentialNoteNonceRandomness(context) {
8890
return [
8991
context.privateState,
9092
wallet.nonceSeed ?? new Uint8Array(getRandomValues(Buffer.alloc(32))),

contracts/test-utils/assertions/test/rejection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
rejectionIncludes,
66
} from '../rejection.js';
77

8-
const WITNESS_REASON = 'wit_Path: commitment not found in tree';
8+
const WITNESS_REASON = 'wit_ConfidentialNotePath: commitment not found in tree';
99

1010
/** The shape the live backend produces: the reason two `cause` levels down. */
1111
const liveWrapped = () => {

0 commit comments

Comments
 (0)