@openzeppelin/compact-contracts 0.4.0 → 0.5.0-alpha.2

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 (52) hide show
  1. package/README.md +9 -4
  2. package/access/AccessControl.compact +8 -9
  3. package/access/Ownable.compact +21 -20
  4. package/access/ShieldedAccessControl.compact +20 -19
  5. package/access/ZOwnablePK.compact +5 -3
  6. package/crypto/Ecdh.compact +153 -0
  7. package/crypto/EcdhMask.compact +146 -84
  8. package/crypto/Ecdsa.compact +1 -1
  9. package/crypto/Eip712.compact +1 -1
  10. package/crypto/ElGamal.compact +13 -13
  11. package/math/UintN.compact +82 -0
  12. package/multisig/ForwarderPrivate.compact +6 -17
  13. package/multisig/ForwarderShielded.compact +1 -1
  14. package/multisig/ForwarderUnshielded.compact +1 -1
  15. package/multisig/ProposalManager.compact +127 -65
  16. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  17. package/multisig/ShieldedTreasury.compact +8 -20
  18. package/multisig/ShieldedTreasuryStateless.compact +9 -21
  19. package/multisig/Signer.compact +1 -1
  20. package/multisig/UnshieldedTreasury.compact +11 -24
  21. package/multisig/examples/ForwarderPrivateExample.compact +2 -3
  22. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  23. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  24. package/multisig/examples/{ShieldedMultiSigV2Example.compact → NativeShieldedStatelessTreasuryExample.compact} +23 -23
  25. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  26. package/multisig/presets/NativeShieldedStatelessTreasury.compact +443 -0
  27. package/multisig/presets/NativeShieldedTokenIssuer.compact +2 -5
  28. package/package.json +11 -11
  29. package/security/Allowlist.compact +1 -1
  30. package/security/Blocklist.compact +1 -1
  31. package/security/Initializable.compact +3 -1
  32. package/security/Pausable.compact +1 -1
  33. package/token/ConfidentialFungibleToken.compact +22 -70
  34. package/token/ConfidentialNoteFungibleToken.compact +741 -0
  35. package/token/FungibleToken.compact +36 -30
  36. package/token/MultiToken.compact +27 -21
  37. package/token/NativeShieldedToken.compact +4 -12
  38. package/token/NativeShieldedTokenCore.compact +1 -1
  39. package/token/NativeShieldedTokenFamily.compact +4 -4
  40. package/token/NonFungibleToken.compact +28 -22
  41. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  42. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  43. package/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact +274 -0
  44. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  45. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +1 -1
  46. package/token/extensions/NativeShieldedTokenPublicSupply.compact +1 -1
  47. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +1 -1
  48. package/token/presets/ZOwnableNativeShieldedToken.compact +1 -1
  49. package/utils/EvmAbi.compact +1 -1
  50. package/utils/Utils.compact +1 -1
  51. package/multisig/RecipientTypes.compact +0 -48
  52. package/multisig/presets/ShieldedMultiSigV2.compact +0 -295
@@ -0,0 +1,274 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/extensions/ConfidentialNoteFungibleTokenDelivery.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module ConfidentialNoteFungibleTokenDelivery
8
+ * @description Extension that publishes every output note
9
+ * of a `ConfidentialNoteFungibleToken` encrypted to its owner's Jubjub key, so
10
+ * a wallet discovers its notes from chain data alone. One ECDH per output, two
11
+ * one-time pads from `EcdhMask.kdf`, one append-only `_deliveries` list. It
12
+ * imports no token module: the composer calls `_deliver` beside the core's
13
+ * note creation.
14
+ *
15
+ * @dev Notation. `Hf`, `pk`, `nonce` and `slot` are defined in the core
16
+ * - `encPk = g^ekScalar`: the owner's Jubjub encryption key, paired with the
17
+ * spend identity `pk` out of band.
18
+ * - `e = Hf("CNFT:delivery:e", seed, slot, nonce, encPk)`: the ephemeral
19
+ * scalar.
20
+ * - `E = g^e` and `S = encPk^e = E^ekScalar`: the `crypto/Ecdh` exchange.
21
+ * - `valueCt = (value + kdf(S, "CNFT:delivery:value")) mod 2^248` and
22
+ * `nonceCt = (nonce + kdf(S, "CNFT:delivery:nonce")) mod 2^248`: one-time
23
+ * pads over `[0, 2^248)`, the width of `kdf`'s output.
24
+ *
25
+ * @dev Compatibility:
26
+ * The wire format a wallet depends on is the `Delivery` layout, the two pad
27
+ * tags, and the modular pad. The `e` tag and the `EphemeralPreimage` layout
28
+ * are sender-side: a recipient recovers `S` from `E` and never recomputes `e`.
29
+ * The compatibility suite pins all of them.
30
+ *
31
+ * @notice Privacy:
32
+ * - Public, per output: `E`, a fresh pseudorandom point, and two pad outputs,
33
+ * each uniform to anyone without `ekScalar`. No field names `pk`, `encPk`
34
+ * or `cm`, so entries cannot be grouped by recipient.
35
+ * - Public, per tx: one entry per `_deliver` call, beside the leaf it
36
+ * describes. The count is constant per entry point when the composer
37
+ * delivers every output (see Composition).
38
+ * - Private: `encPk`, `value`, `nonce`, the seed, `e`, `S`. `slot` is hashed
39
+ * into `e` and never written; it is a public constant of the composer.
40
+ * - Hiding: with a uniform mask the pad is perfect, so `valueCt` and
41
+ * `nonceCt` carry nothing about the plaintext, regardless of its width
42
+ * or distribution; the only bound is `2^248` (`UintN<248>.MODULUS`). The mask's
43
+ * uniformity is `EcdhMask`'s assumption: hashed ElGamal, CDH in the
44
+ * random-oracle model.
45
+ *
46
+ * @notice Security:
47
+ * - The nonce is the spend capability and the pads are one-time, so a
48
+ * repeated `e` to one key opens both notes. `e` binds the nonce and the
49
+ * key: two committed notes never share a pad even under a reused seed,
50
+ * since `_issuedNonces` keeps nonces unique, and one note to two keys
51
+ * never shares `E`. The seed MUST still be fresh and secret per call. `e`
52
+ * is secret only while the seed or the nonce is, and a known `e` opens the
53
+ * entry to everyone.
54
+ * - No authentication: the pads are malleable and nothing binds an entry to a
55
+ * leaf. A wallet accepts a recovered note only once `commitOf(note, pk)` is
56
+ * in the tree (see Client-side duties).
57
+ * - No authorization: `_deliver` is ungated like the core's `_` circuits. The
58
+ * composer's gate on note creation is the gate on delivery.
59
+ * - Not checked here: that the delivered pair is the committed note, and that
60
+ * `encPk` pairs with the owner's `pk`. The composer passes one note to
61
+ * `_mintNote` and to `_deliver` in one circuit. A wrong `encPk` makes the
62
+ * note undiscoverable, not stealable, since `cm` binds the owner's `pk`.
63
+ *
64
+ * @notice Concurrency:
65
+ * `_deliver` reads nothing and pins nothing, so every delivery commutes with
66
+ * every other call.
67
+ *
68
+ * @notice Composition:
69
+ * - Call `_deliver` once per output and per intended key, zero-value change
70
+ * included, never under a condition on private data: the entry count per
71
+ * entry point must stay constant.
72
+ * - `slot` separates repeated deliveries of one note to one key. Under a
73
+ * reused seed, the same note to the same key with one slot repeats the
74
+ * entry byte for byte; distinct slots give each delivery its own `E`.
75
+ * - Nothing to initialize and no ledger read: `_deliver` runs from the
76
+ * composer's constructor, which is how a constructor-time mint reaches its
77
+ * owner.
78
+ * - Delivering a note twice adds an entry and nothing else. Wallets dedupe by
79
+ * commitment.
80
+ * - A composed contract supersedes the core header's out-of-band delivery and
81
+ * no-rescan statements: its wallets find their notes by scanning
82
+ * `_deliveries`.
83
+ *
84
+ * @notice Client-side duties:
85
+ * - Keys: `encPk = g^ekScalar`. Derive `ekScalar` from the wallet seed under a
86
+ * module-specific domain (`ElGamal.secretToScalar` is the library's
87
+ * derivation) and share `(pk, encPk)` together.
88
+ * - Scan: read `_deliveries` from the length last seen, newest first. For each
89
+ * entry, `recoverNote(entry, ekScalar)`; drop `value >= 2^128`, a wrong key
90
+ * with overwhelming probability; keep the note iff
91
+ * `commitOf({ value, nonce }, pk)` is a leaf, and record the leaf index;
92
+ * `nullifierOf(note)` in `_nullifiers` means spent. One scalar
93
+ * multiplication per entry, no proof.
94
+ * - `ekScalar` never enters an on-chain circuit: `recoverNote` is pure and
95
+ * runs in the wallet.
96
+ *
97
+ * @notice Scalability:
98
+ * `_deliveries` gains one entry per output and is never pruned; every wallet
99
+ * scans every entry once.
100
+ *
101
+ * @dev Performance:
102
+ * `_deliver` pays one SHA-256 over the ephemeral preimage, two scalar
103
+ * multiplications, one point digest, two masks derived from it and the two
104
+ * modular pads.
105
+ */
106
+ module ConfidentialNoteFungibleTokenDelivery {
107
+ import CompactStandardLibrary;
108
+ import "../../crypto/Ecdh" prefix Ecdh_;
109
+ import "../../crypto/EcdhMask" prefix EcdhMask_;
110
+ import { wrappingAdd, wrappingSub } from "../../math/UintN"<248> prefix U248_;
111
+
112
+ /**
113
+ * @description One output note, encrypted to its owner. Public once written;
114
+ * no field is secret on its own.
115
+ * @type {JubjubPoint} ephemeralPk - `E = g^e`, fresh per output. Never the
116
+ * identity.
117
+ * @type {Uint<248>} valueCt - `(value + kdf(S, "CNFT:delivery:value")) mod 2^248`.
118
+ * @type {Uint<248>} nonceCt - `(nonce + kdf(S, "CNFT:delivery:nonce")) mod 2^248`.
119
+ *
120
+ * @dev The `Uint<248>` pads keep both ciphertext atoms below 32 bytes. The
121
+ * generated `List` accessors (`head`, `popFront`) throw `exceeded structure
122
+ * bounds` on an entry with a full-width field atom.
123
+ */
124
+ export struct Delivery {
125
+ ephemeralPk: JubjubPoint;
126
+ valueCt: Uint<248>;
127
+ nonceCt: Uint<248>;
128
+ }
129
+
130
+ /**
131
+ * @description An entry with its pads stripped, nothing checked. A wrong key
132
+ * yields a uniform pair; the wallet range-checks `value` and matches the
133
+ * commitment.
134
+ * @type {Uint<248>} value - The note's value, below `2^128` for a note
135
+ * addressed to the key.
136
+ * @type {Uint<248>} nonce - The note's nonce.
137
+ */
138
+ export struct RecoveredNote {
139
+ value: Uint<248>;
140
+ nonce: Uint<248>;
141
+ }
142
+
143
+ /**
144
+ * @description Preimage of the ephemeral scalar: the core's `NoncePreimage`
145
+ * shape plus the recipient key. The domain tag and the extra field keep `e`
146
+ * apart from a core nonce drawn from the same seed.
147
+ * @type {Bytes<32>} domain - The ephemeral domain tag.
148
+ * @type {Bytes<32>} seed - The caller's per-invocation randomness.
149
+ * @type {Bytes<32>} slot - The per-output tag.
150
+ * @type {Field} nonce - The delivered note's nonce.
151
+ * @type {JubjubPoint} encPk - The key the entry is addressed to.
152
+ */
153
+ struct EphemeralPreimage {
154
+ domain: Bytes<32>;
155
+ seed: Bytes<32>;
156
+ slot: Bytes<32>;
157
+ nonce: Field;
158
+ encPk: JubjubPoint;
159
+ }
160
+
161
+ /**
162
+ * @description Every delivery ever made, newest first. Append-only: no
163
+ * circuit reads, removes or overwrites an entry. Unbounded.
164
+ * @type {List<Delivery>} _deliveries - The feed wallets scan.
165
+ */
166
+ export ledger _deliveries: List<Delivery>;
167
+
168
+ /**
169
+ * @witness wit_ConfidentialNoteDeliveryRandomness
170
+ * @description Returns the seed `_deliver` derives its ephemeral scalar
171
+ * from.
172
+ *
173
+ * @dev Freshness is mandatory:
174
+ * MUST be fresh and secret per call. The nonce binding turns a reused seed
175
+ * from a stolen note into nothing; `e` still stays secret only while the
176
+ * seed or the nonce does.
177
+ *
178
+ * @returns {Bytes<32>} seed - A 32-byte cryptographically secure random
179
+ * value.
180
+ */
181
+ witness wit_ConfidentialNoteDeliveryRandomness(): Bytes<32>;
182
+
183
+ /**
184
+ * @description Publishes `(value, nonce)` encrypted to `encPk`: one ECDH
185
+ * under a key- and nonce-bound ephemeral, two modular `EcdhMask` pads, one
186
+ * `_deliveries` entry. Call it beside the core's note creation, once per
187
+ * output and intended key.
188
+ *
189
+ * @constraints k=15, rows=24782
190
+ *
191
+ * @warning No authorization:
192
+ * The composer gates who may call this.
193
+ *
194
+ * @dev Key- and nonce-bound ephemeral:
195
+ * `e = Hf(domain, seed, slot, nonce, encPk)`, so `e` repeats only when all
196
+ * of `(seed, slot, nonce, encPk)` repeat: a reused seed never pads two
197
+ * committed notes alike and never gives two keys one `E`.
198
+ *
199
+ * @dev Constant shape:
200
+ * No branch on the ledger and no ledger read: the transcript is independent
201
+ * of every input, and two deliveries in one block never conflict.
202
+ *
203
+ * Requirements:
204
+ *
205
+ * - `encPk` is not the identity point.
206
+ * - `nonce < 2^248`. Every `degradeToTransient` output qualifies.
207
+ *
208
+ * @param {JubjubPoint} encPk - The owner's encryption key, `g^ekScalar`.
209
+ * @param {Uint<128>} value - The note's value.
210
+ * @param {Field} nonce - The note's nonce.
211
+ * @param {Bytes<32>} slot - The per-output tag; distinct per delivery when
212
+ * one note goes to one key more than once.
213
+ * @return {[]} - Empty tuple.
214
+ */
215
+ export circuit _deliver(encPk: JubjubPoint, value: Uint<128>, nonce: Field, slot: Bytes<32>): [] {
216
+ const e = degradeToTransient(persistentHash<EphemeralPreimage>(EphemeralPreimage {
217
+ seed: wit_ConfidentialNoteDeliveryRandomness(),
218
+ domain: pad(32, "CNFT:delivery:e"),
219
+ slot: slot,
220
+ nonce: nonce,
221
+ encPk: encPk
222
+ })) as JubjubScalar;
223
+ // The identity-key and zero-ephemeral guards come from `deriveShared`.
224
+ const shared = Ecdh_deriveShared(encPk, e);
225
+ // One point digest feeds both pads.
226
+ const sharedDigest = EcdhMask_pointDigest(shared.sShared);
227
+ // The cast is the width check: a bit above the pad would sit in the clear.
228
+ const paddedNonce = nonce as Uint<248>;
229
+ const delivery = Delivery {
230
+ ephemeralPk: shared.ephemeralPk,
231
+ valueCt: U248_wrappingAdd(value as Uint<248>,
232
+ EcdhMask_kdfFromDigest(sharedDigest, pad(32, "CNFT:delivery:value")) as Uint<248>),
233
+ nonceCt: U248_wrappingAdd(paddedNonce,
234
+ EcdhMask_kdfFromDigest(sharedDigest, pad(32, "CNFT:delivery:nonce")) as Uint<248>)
235
+ };
236
+ _deliveries.pushFront(disclose(delivery));
237
+ }
238
+
239
+ /**
240
+ * @description Strips the pads from an entry with the owner's secret scalar.
241
+ * Pure and off-chain: a wallet reads its notes with no proof.
242
+ *
243
+ * @constraints k=14, rows=15063
244
+ *
245
+ * @notice Raw result:
246
+ * Nothing derived from `ekScalar` or the recovered pair is checked, so the
247
+ * circuit is no oracle for a candidate key. A wrong key returns a uniform
248
+ * pair; the wallet filters on `value < 2^128` and the commitment tree.
249
+ *
250
+ * @dev Identity ephemeral:
251
+ * `_deliver` never writes one, and its pads would be public constants, so
252
+ * the entry is refused rather than opened, as `EcdhMask.decrypt` does.
253
+ *
254
+ * Requirements:
255
+ *
256
+ * - `delivery.ephemeralPk` is not the identity point.
257
+ *
258
+ * @param {Delivery} delivery - The entry to open.
259
+ * @param {JubjubScalar} ekScalar - The secret behind the `encPk` the entry
260
+ * was addressed to.
261
+ * @return {RecoveredNote} - The padded pair.
262
+ */
263
+ export pure circuit recoverNote(delivery: Delivery, ekScalar: JubjubScalar): RecoveredNote {
264
+ assert(delivery.ephemeralPk != ecMulGenerator(0 as JubjubScalar),
265
+ "ConfidentialNoteFungibleTokenDelivery: identity ephemeral");
266
+ const sharedDigest = EcdhMask_pointDigest(Ecdh_recoverShared(delivery.ephemeralPk, ekScalar));
267
+ return RecoveredNote {
268
+ value: U248_wrappingSub(delivery.valueCt,
269
+ EcdhMask_kdfFromDigest(sharedDigest, pad(32, "CNFT:delivery:value")) as Uint<248>),
270
+ nonce: U248_wrappingSub(delivery.nonceCt,
271
+ EcdhMask_kdfFromDigest(sharedDigest, pad(32, "CNFT:delivery:nonce")) as Uint<248>)
272
+ };
273
+ }
274
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (token/extensions/NativeShieldedTokenDerivedNonce.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/extensions/NativeShieldedTokenDerivedNonce.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenFamilyPublicSupply.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/extensions/NativeShieldedTokenFamilyPublicSupply.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenPublicSupply.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/extensions/NativeShieldedTokenPublicSupply.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenPublicSupplyCore.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/extensions/NativeShieldedTokenPublicSupplyCore.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.4 (token/presets/ZOwnableNativeShieldedToken.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/presets/ZOwnableNativeShieldedToken.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (utils/EvmAbi.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (utils/EvmAbi.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (utils/Utils.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (utils/Utils.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,48 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (multisig/RecipientTypes.compact)
3
-
4
- pragma language_version >= 0.26.0;
5
-
6
- /**
7
- * @module RecipientTypes
8
- * @description The transfer-target types shared by the multisig modules.
9
- *
10
- * This module holds no ledger state. Importing a module allocates every
11
- * ledger field it declares, so a contract that needs only the recipient
12
- * types imports this module rather than `ProposalManager`, which would add
13
- * a `_nextProposalId` and `_proposals` the contract never writes.
14
- * `ProposalManager` re-exports both types and provides the typed helper
15
- * circuits that build and route recipients.
16
- */
17
- module RecipientTypes {
18
- import CompactStandardLibrary;
19
-
20
- /**
21
- * @description What a `Recipient`'s bytes are and which rail accepts them.
22
- *
23
- * - `ShieldedUser`: a Zswap coin public key. `ProposalManager.toShieldedRecipient` only.
24
- * - `UnshieldedUser`: a user address. `ProposalManager.toUnshieldedRecipient` only.
25
- * - `Contract`: a contract address. Accepted by both.
26
- *
27
- * @warning `toShieldedRecipient` rejects `UnshieldedUser` and
28
- * `toUnshieldedRecipient` rejects `ShieldedUser`, both at execution rather
29
- * than at creation. That mismatch is the hazard noted in
30
- * `ProposalManager._createProposal`.
31
- */
32
- export enum RecipientKind {
33
- ShieldedUser,
34
- UnshieldedUser,
35
- Contract
36
- }
37
-
38
- /**
39
- * @description A transfer target.
40
- *
41
- * @warning `address` is raw 32 bytes. `kind` alone decides whether they are
42
- * read as a Zswap coin public key, a user address, or a contract address.
43
- */
44
- export struct Recipient {
45
- kind: RecipientKind,
46
- address: Bytes<32>
47
- }
48
- }
@@ -1,295 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (multisig/presets/ShieldedMultiSigV2.compact)
3
-
4
- pragma language_version >= 0.26.0;
5
-
6
- /**
7
- * @module ShieldedMultiSigV2
8
- * @description Privacy-preserving threshold multisig contract.
9
- *
10
- * Signer identities are stored as commitments: hashes of ECDSA public
11
- * keys combined with an instance salt and domain separator. Signature
12
- * verification happens in a single transaction with no multi-step
13
- * proposal lifecycle. The contract enforces threshold authorization
14
- * and replay protection. All other coordination (signature collection,
15
- * coin selection) happens off-chain.
16
- *
17
- * `t` is the approval width and the threshold: `execute` verifies exactly
18
- * `t` distinct signers. Import as `"../presets/ShieldedMultiSigV2"<t>`; the
19
- * implementing contract fixes the signer count `n` at `initialize`. Import
20
- * one width per contract: two widths share one signer registry, see
21
- * `Secp256k1EcdsaMultisig`.
22
- *
23
- * Treasury is fully stateless meaning coin data is not stored on the public ledger.
24
- * Deposits call receiveShielded only. The operator discovers coin indices
25
- * through ZswapOutput events from the indexer, constructs QualifiedShieldedCoinInfo
26
- * off-chain, and provides it as a circuit parameter for spending.
27
- *
28
- * @notice Keeping coin data off the ledger hides balances from the public,
29
- * not from a depositor. Whoever knows a deposited coin can find the `execute`
30
- * that spends it, recover the amount sent, test candidate recipients against
31
- * the sent coin's commitment, and derive the change coin, so every later
32
- * spend descending from that change and the balance it carries are traceable
33
- * by that depositor. See `ShieldedTreasuryStateless` for the derivation.
34
- *
35
- * @notice Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
36
- *
37
- * @notice Implementing contracts register the signer set from their
38
- * constructor by calling this module's `initialize`, and MUST NOT import
39
- * `Signer` or `Secp256k1EcdsaMultisig` directly.
40
- */
41
- module ShieldedMultiSigV2<#t> {
42
- import CompactStandardLibrary;
43
-
44
- import "../Secp256k1EcdsaMultisig"<t> prefix Secp256k1EcdsaMultisig_;
45
- import "../../crypto/Eip712" prefix Eip712_;
46
- import "../../utils/EvmAbi" prefix EvmAbi_;
47
- import "../RecipientTypes";
48
- import "../ShieldedTreasuryStateless" prefix Treasury_;
49
-
50
- // Implementing contracts need these for the re-exported `execute` signature.
51
- export { Recipient, RecipientKind };
52
-
53
- /**
54
- * @description Monotonic operation counter. `execute` reads it as that
55
- * operation's nonce and then increments, so no two executions share one.
56
- */
57
- ledger _nonce: Counter;
58
-
59
- /**
60
- * @description The EIP-712 domain separator every `execute` digest is built
61
- * under, derived from `(name, version, instanceSalt)`. Set once at
62
- * construction.
63
- */
64
- export sealed ledger _domainSeparator: Bytes<32>;
65
-
66
- /**
67
- * @description Registers the signer set at threshold `t` and fixes the
68
- * EIP-712 domain separator.
69
- *
70
- * @notice Call from the implementing contract's constructor only. It writes
71
- * the `sealed` `_domainSeparator`, so implementing contracts MUST NOT expose
72
- * `initialize` as a post-deploy circuit.
73
- *
74
- * The EIP-712 domain fixed here is `(name, version, salt)`. `chainId` and
75
- * `verifyingContract` are absent: no network identifier is available
76
- * in-circuit, and a 32-byte Midnight address does not fit
77
- * `verifyingContract`'s `address` type.
78
- *
79
- * Replay protection comes from `execute` binding `kernel.self()` in its
80
- * struct rather than from the domain. The ledger randomizes a contract's
81
- * address at deployment, so no two deployments share one on any network.
82
- * A signature for one cannot authorize another.
83
- *
84
- * `instanceSalt` also keys the signer commitments, so a fresh value per
85
- * deployment keeps one public key from producing the same commitment in two
86
- * deployments which would link the signer set across them.
87
- *
88
- * Requirements:
89
- *
90
- * - `instanceSalt` should be cryptographically random.
91
- * - `signerCommitments` must not contain duplicates.
92
- * - `n` must be at least `t`.
93
- *
94
- * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
95
- * @param {Vector<n, Bytes<32>>} signerCommitments - Hashed signer identities.
96
- * @returns {[]} Empty tuple.
97
- */
98
- export circuit initialize<#n>(
99
- instanceSalt: Bytes<32>,
100
- signerCommitments: Vector<n, Bytes<32>>
101
- ): [] {
102
- Secp256k1EcdsaMultisig_initialize<n>(instanceSalt, signerCommitments);
103
-
104
- const hashedName = keccak256<Bytes<18>>(pad(18, "ShieldedMultiSigV2"));
105
- const hashedVersion = keccak256<Bytes<1>>(pad(1, "1"));
106
- _domainSeparator = Eip712_domainSeparator(
107
- hashedName,
108
- hashedVersion,
109
- disclose(instanceSalt)
110
- );
111
- }
112
-
113
- /**
114
- * @description `keccak256` of the `Execute` type string:
115
- * `Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)`
116
- *
117
- * @dev Written as a literal word. `compactc` emits a full keccak
118
- * permutation even for a constant argument, so hashing the type string
119
- * in-circuit would add ~4.2k rows to every `execute`. The spec checks this
120
- * word against the type string above.
121
- *
122
- * @returns {Bytes<32>} The `Execute` type hash.
123
- */
124
- pure circuit EXECUTE_TYPEHASH(): Bytes<32> {
125
- const typeHash: Vector<32, Uint<8>> = [
126
- 0x8e, 0x36, 0xc9, 0x78, 0xf2, 0x00, 0x15, 0xe8,
127
- 0x1d, 0x7a, 0x53, 0xef, 0xc3, 0x2e, 0x2b, 0x08,
128
- 0xf1, 0x24, 0x97, 0xa5, 0x05, 0x7c, 0x70, 0x20,
129
- 0x7f, 0x8b, 0xc4, 0xb7, 0xc6, 0xcf, 0x5e, 0x15
130
- ];
131
-
132
- return typeHash as Bytes<32>;
133
- }
134
-
135
- /**
136
- * @description Receives a shielded coin into the multisig treasury.
137
- *
138
- * No access control which allows anyone to deposit. The coin is claimed at the
139
- * protocol level through receiveShielded. No coin data is stored on the
140
- * public ledger, so no balance is readable from it; a depositor can still
141
- * follow the coin they deposited (see the module notice).
142
- *
143
- * The operator discovers the coin's Merkle tree index by subscribing
144
- * to ZswapOutput events via the indexer, filtering by contract address,
145
- * and extracting mt_index. Combined with the known ShieldedCoinInfo,
146
- * this produces the QualifiedShieldedCoinInfo needed for spending.
147
- *
148
- * @constraints k=13, rows=5898 (zkir-v3)
149
- *
150
- * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
151
- */
152
- export circuit deposit(coin: ShieldedCoinInfo): [] {
153
- receiveShielded(disclose(coin));
154
- }
155
-
156
- /**
157
- * @description Executes a shielded send authorized by threshold signatures.
158
- *
159
- * The circuit reads the current nonce from the ledger, increments it,
160
- * then reconstructs the EIP-712 digest that signers must have signed
161
- * off-chain: the `Execute` struct (contract address, nonce, recipient kind,
162
- * recipient, coin color, amount) hashed under this deployment's domain
163
- * separator. The digest is bound to the contract instance and the full recipient (kind and
164
- * address), so a signature authorizes exactly one operation on one instance.
165
- *
166
- * Approvals are checked by `Secp256k1EcdsaMultisig`, which derives each
167
- * signer's commitment from their public key and the instance salt and
168
- * rejects duplicates. Constraints below are measured at `t = 2`.
169
- *
170
- * @notice `to` must be a `ShieldedUser`. A contract-addressed coin would be
171
- * unclaimable, making the transaction malformed.
172
- *
173
- * @constraints k=17, rows=98969 (zkir-v3)
174
- *
175
- * Requirements:
176
- *
177
- * - No two entries may be the same signer.
178
- * - Every public key must hash to a registered signer commitment.
179
- * - Every signature must be valid over the message hash and use the low-s
180
- * encoding.
181
- * - `to.kind` must be `ShieldedUser`.
182
- * - Coin value must be >= amount.
183
- *
184
- * @param {Recipient} to - The recipient.
185
- * @param {Uint<128>} amount - The amount to send.
186
- * @param {QualifiedShieldedCoinInfo} coin - The coin to spend (from operator's pool).
187
- * @param {Vector<t, Secp256k1Point>} pubkeys - Public keys of approving signers.
188
- * @param {Vector<t, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the operation.
189
- *
190
- * @returns {ShieldedSendResult} The send result including any change.
191
- */
192
- export circuit execute(
193
- to: Recipient,
194
- amount: Uint<128>,
195
- coin: QualifiedShieldedCoinInfo,
196
- pubkeys: Vector<t, Secp256k1Point>,
197
- signatures: Vector<t, Secp256k1EcdsaSignature>
198
- ): ShieldedSendResult {
199
- // Increment nonce
200
- const currentNonce = _nonce;
201
- _nonce.increment(1);
202
-
203
- // Construct message hash
204
- const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<7, Bytes<32>>>([
205
- EXECUTE_TYPEHASH(),
206
- // Binds the digest to this deployment. The domain cannot carry the
207
- // address, so removing this word removes cross-instance replay
208
- // protection.
209
- kernel.self().bytes,
210
- EvmAbi_uint64Word(currentNonce),
211
- EvmAbi_uint8Word(to.kind as Uint<8>),
212
- to.address,
213
- coin.color,
214
- EvmAbi_uint128Word(amount)
215
- ]));
216
-
217
- Secp256k1EcdsaMultisig_assertApprovals(msgHash, pubkeys, signatures);
218
-
219
- assert(
220
- to.kind == RecipientKind.ShieldedUser,
221
- "ShieldedMultiSigV2: recipient must be a coin public key"
222
- );
223
- const recipient = left<ZswapCoinPublicKey, ContractAddress>(
224
- ZswapCoinPublicKey { bytes: to.address }
225
- );
226
- return Treasury__send(coin, recipient, amount);
227
- }
228
-
229
- /**
230
- * @description Computes a signer commitment from a secp256k1 public key,
231
- * as `persistentHash(pkX, pkY, salt, "Secp256k1EcdsaMultisig:signer")`.
232
- *
233
- * Pure, so the deployer can derive constructor commitments off-chain.
234
- *
235
- * Requirements:
236
- *
237
- * - `pk` must not be the secp256k1 identity point, which has no coordinates.
238
- *
239
- * @param {Secp256k1Point} pk - The secp256k1 public key.
240
- * @param {Bytes<32>} salt - The instance salt.
241
- *
242
- * @returns {Bytes<32>} The signer commitment.
243
- */
244
- export pure circuit _calculateSignerId(
245
- pk: Secp256k1Point,
246
- salt: Bytes<32>
247
- ): Bytes<32> {
248
- return Secp256k1EcdsaMultisig_calculateSignerId(pk, salt);
249
- }
250
-
251
- /**
252
- * @description Returns the nonce the next `execute` digest will bind to.
253
- *
254
- * @constraints k=6, rows=34 (zkir-v3)
255
- *
256
- * @returns {Uint<64>} The current nonce.
257
- */
258
- export circuit getNonce(): Uint<64> {
259
- return _nonce;
260
- }
261
-
262
- /**
263
- * @description Returns the number of registered signers.
264
- *
265
- * @constraints k=6, rows=34 (zkir-v3)
266
- *
267
- * @returns {Uint<8>} The signer count.
268
- */
269
- export circuit getSignerCount(): Uint<8> {
270
- return Secp256k1EcdsaMultisig_getSignerCount();
271
- }
272
-
273
- /**
274
- * @description Returns the approval threshold.
275
- *
276
- * @constraints k=6, rows=34 (zkir-v3)
277
- *
278
- * @returns {Uint<8>} The threshold.
279
- */
280
- export circuit getThreshold(): Uint<8> {
281
- return Secp256k1EcdsaMultisig_getThreshold();
282
- }
283
-
284
- /**
285
- * @description Returns whether the given commitment is a registered signer.
286
- *
287
- * @constraints k=8, rows=129 (zkir-v3)
288
- *
289
- * @param {Bytes<32>} commitment - The signer commitment to check.
290
- * @returns {Boolean} True if registered.
291
- */
292
- export circuit isSigner(commitment: Bytes<32>): Boolean {
293
- return Secp256k1EcdsaMultisig_isSigner(commitment);
294
- }
295
- }