@openzeppelin/compact-contracts 0.5.0-alpha.1 → 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 (51) hide show
  1. package/README.md +9 -4
  2. package/access/AccessControl.compact +8 -9
  3. package/access/Ownable.compact +19 -20
  4. package/access/ShieldedAccessControl.compact +1 -1
  5. package/access/ZOwnablePK.compact +3 -3
  6. package/crypto/Ecdh.compact +1 -1
  7. package/crypto/EcdhMask.compact +61 -22
  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 +1 -1
  13. package/multisig/ForwarderShielded.compact +1 -1
  14. package/multisig/ForwarderUnshielded.compact +1 -1
  15. package/multisig/ProposalManager.compact +3 -3
  16. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  17. package/multisig/ShieldedTreasury.compact +1 -1
  18. package/multisig/ShieldedTreasuryStateless.compact +5 -11
  19. package/multisig/Signer.compact +1 -1
  20. package/multisig/UnshieldedTreasury.compact +1 -1
  21. package/multisig/examples/ForwarderPrivateExample.compact +1 -1
  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 +1 -1
  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 +1 -1
  32. package/security/Pausable.compact +1 -1
  33. package/token/ConfidentialFungibleToken.compact +1 -1
  34. package/token/ConfidentialNoteFungibleToken.compact +1 -1
  35. package/token/FungibleToken.compact +34 -30
  36. package/token/MultiToken.compact +25 -21
  37. package/token/NativeShieldedToken.compact +2 -2
  38. package/token/NativeShieldedTokenCore.compact +1 -1
  39. package/token/NativeShieldedTokenFamily.compact +4 -4
  40. package/token/NonFungibleToken.compact +26 -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/presets/ShieldedMultiSigV2.compact +0 -289
@@ -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.5.0-alpha.1 (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.5.0-alpha.1 (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.5.0-alpha.1 (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,289 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (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 Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
29
- *
30
- * @notice Implementing contracts register the signer set from their
31
- * constructor by calling this module's `initialize`, and MUST NOT import
32
- * `Signer` or `Secp256k1EcdsaMultisig` directly.
33
- */
34
- module ShieldedMultiSigV2<#t> {
35
- import CompactStandardLibrary;
36
-
37
- import "../Secp256k1EcdsaMultisig"<t> prefix Secp256k1EcdsaMultisig_;
38
- import "../../crypto/Eip712" prefix Eip712_;
39
- import "../../utils/EvmAbi" prefix EvmAbi_;
40
- import "../ProposalManager" prefix Proposal_;
41
- import "../ShieldedTreasuryStateless" prefix Treasury_;
42
-
43
- // Implementing contracts need this for the re-exported `execute` signature.
44
- // Importing `ProposalManager` again instead would give them a second,
45
- // unwritten copy of its state.
46
- export { Proposal_Recipient };
47
-
48
- /**
49
- * @description Monotonic operation counter. `execute` reads it as that
50
- * operation's nonce and then increments, so no two executions share one.
51
- */
52
- ledger _nonce: Counter;
53
-
54
- /**
55
- * @description The EIP-712 domain separator every `execute` digest is built
56
- * under, derived from `(name, version, instanceSalt)`. Set once at
57
- * construction.
58
- */
59
- export sealed ledger _domainSeparator: Bytes<32>;
60
-
61
- /**
62
- * @description Registers the signer set at threshold `t` and fixes the
63
- * EIP-712 domain separator.
64
- *
65
- * @notice Call from the implementing contract's constructor only. It writes
66
- * the `sealed` `_domainSeparator`, so implementing contracts MUST NOT expose
67
- * `initialize` as a post-deploy circuit.
68
- *
69
- * The EIP-712 domain fixed here is `(name, version, salt)`. `chainId` and
70
- * `verifyingContract` are absent: no network identifier is available
71
- * in-circuit, and a 32-byte Midnight address does not fit
72
- * `verifyingContract`'s `address` type.
73
- *
74
- * Replay protection comes from `execute` binding `kernel.self()` in its
75
- * struct rather than from the domain. The ledger randomizes a contract's
76
- * address at deployment, so no two deployments share one on any network.
77
- * A signature for one cannot authorize another.
78
- *
79
- * `instanceSalt` also keys the signer commitments, so a fresh value per
80
- * deployment keeps one public key from producing the same commitment in two
81
- * deployments which would link the signer set across them.
82
- *
83
- * Requirements:
84
- *
85
- * - `instanceSalt` should be cryptographically random.
86
- * - `signerCommitments` must not contain duplicates.
87
- * - `n` must be at least `t`.
88
- *
89
- * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
90
- * @param {Vector<n, Bytes<32>>} signerCommitments - Hashed signer identities.
91
- * @returns {[]} Empty tuple.
92
- */
93
- export circuit initialize<#n>(
94
- instanceSalt: Bytes<32>,
95
- signerCommitments: Vector<n, Bytes<32>>
96
- ): [] {
97
- Secp256k1EcdsaMultisig_initialize<n>(instanceSalt, signerCommitments);
98
-
99
- const hashedName = keccak256<Bytes<18>>(pad(18, "ShieldedMultiSigV2"));
100
- const hashedVersion = keccak256<Bytes<1>>(pad(1, "1"));
101
- _domainSeparator = Eip712_domainSeparator(
102
- hashedName,
103
- hashedVersion,
104
- disclose(instanceSalt)
105
- );
106
- }
107
-
108
- /**
109
- * @description `keccak256` of the `Execute` type string:
110
- * `Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)`
111
- *
112
- * @dev Written as a literal word. `compactc` emits a full keccak
113
- * permutation even for a constant argument, so hashing the type string
114
- * in-circuit would add ~4.2k rows to every `execute`. The spec checks this
115
- * word against the type string above.
116
- *
117
- * @returns {Bytes<32>} The `Execute` type hash.
118
- */
119
- pure circuit EXECUTE_TYPEHASH(): Bytes<32> {
120
- const typeHash: Vector<32, Uint<8>> = [
121
- 0x8e, 0x36, 0xc9, 0x78, 0xf2, 0x00, 0x15, 0xe8,
122
- 0x1d, 0x7a, 0x53, 0xef, 0xc3, 0x2e, 0x2b, 0x08,
123
- 0xf1, 0x24, 0x97, 0xa5, 0x05, 0x7c, 0x70, 0x20,
124
- 0x7f, 0x8b, 0xc4, 0xb7, 0xc6, 0xcf, 0x5e, 0x15
125
- ];
126
-
127
- return typeHash as Bytes<32>;
128
- }
129
-
130
- /**
131
- * @description Receives a shielded coin into the multisig treasury.
132
- *
133
- * No access control which allows anyone to deposit. The coin is claimed at the
134
- * protocol level through receiveShielded. No coin data is stored on the
135
- * public ledger, preserving full balance privacy.
136
- *
137
- * The operator discovers the coin's Merkle tree index by subscribing
138
- * to ZswapOutput events via the indexer, filtering by contract address,
139
- * and extracting mt_index. Combined with the known ShieldedCoinInfo,
140
- * this produces the QualifiedShieldedCoinInfo needed for spending.
141
- *
142
- * @constraints k=13, rows=5898 (zkir-v3)
143
- *
144
- * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
145
- */
146
- export circuit deposit(coin: ShieldedCoinInfo): [] {
147
- receiveShielded(disclose(coin));
148
- }
149
-
150
- /**
151
- * @description Executes a shielded send authorized by threshold signatures.
152
- *
153
- * The circuit reads the current nonce from the ledger, increments it,
154
- * then reconstructs the EIP-712 digest that signers must have signed
155
- * off-chain: the `Execute` struct (contract address, nonce, recipient kind,
156
- * recipient, coin color, amount) hashed under this deployment's domain
157
- * separator. The digest is bound to the contract instance and the full recipient (kind and
158
- * address), so a signature authorizes exactly one operation on one instance.
159
- *
160
- * Approvals are checked by `Secp256k1EcdsaMultisig`, which derives each
161
- * signer's commitment from their public key and the instance salt and
162
- * rejects duplicates. Constraints below are measured at `t = 2`.
163
- *
164
- * @notice `to` must be a `ShieldedUser`. A contract-addressed coin would be
165
- * unclaimable, making the transaction malformed.
166
- *
167
- * @constraints k=17, rows=98969 (zkir-v3)
168
- *
169
- * Requirements:
170
- *
171
- * - No two entries may be the same signer.
172
- * - Every public key must hash to a registered signer commitment.
173
- * - Every signature must be valid over the message hash and use the low-s
174
- * encoding.
175
- * - `to.kind` must be `ShieldedUser`.
176
- * - Coin value must be >= amount.
177
- *
178
- * @param {Proposal_Recipient} to - The recipient.
179
- * @param {Uint<128>} amount - The amount to send.
180
- * @param {QualifiedShieldedCoinInfo} coin - The coin to spend (from operator's pool).
181
- * @param {Vector<t, Secp256k1Point>} pubkeys - Public keys of approving signers.
182
- * @param {Vector<t, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the operation.
183
- *
184
- * @returns {ShieldedSendResult} The send result including any change.
185
- */
186
- export circuit execute(
187
- to: Proposal_Recipient,
188
- amount: Uint<128>,
189
- coin: QualifiedShieldedCoinInfo,
190
- pubkeys: Vector<t, Secp256k1Point>,
191
- signatures: Vector<t, Secp256k1EcdsaSignature>
192
- ): ShieldedSendResult {
193
- // Increment nonce
194
- const currentNonce = _nonce;
195
- _nonce.increment(1);
196
-
197
- // Construct message hash
198
- const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<7, Bytes<32>>>([
199
- EXECUTE_TYPEHASH(),
200
- // Binds the digest to this deployment. The domain cannot carry the
201
- // address, so removing this word removes cross-instance replay
202
- // protection.
203
- kernel.self().bytes,
204
- EvmAbi_uint64Word(currentNonce),
205
- EvmAbi_uint8Word(to.kind as Uint<8>),
206
- to.address,
207
- coin.color,
208
- EvmAbi_uint128Word(amount)
209
- ]));
210
-
211
- Secp256k1EcdsaMultisig_assertApprovals(msgHash, pubkeys, signatures);
212
-
213
- assert(
214
- to.kind == Proposal_RecipientKind.ShieldedUser,
215
- "ShieldedMultiSigV2: recipient must be a coin public key"
216
- );
217
- const recipient = left<ZswapCoinPublicKey, ContractAddress>(
218
- ZswapCoinPublicKey { bytes: to.address }
219
- );
220
- return Treasury__send(coin, recipient, amount);
221
- }
222
-
223
- /**
224
- * @description Computes a signer commitment from a secp256k1 public key,
225
- * as `persistentHash(pkX, pkY, salt, "Secp256k1EcdsaMultisig:signer")`.
226
- *
227
- * Pure, so the deployer can derive constructor commitments off-chain.
228
- *
229
- * Requirements:
230
- *
231
- * - `pk` must not be the secp256k1 identity point, which has no coordinates.
232
- *
233
- * @param {Secp256k1Point} pk - The secp256k1 public key.
234
- * @param {Bytes<32>} salt - The instance salt.
235
- *
236
- * @returns {Bytes<32>} The signer commitment.
237
- */
238
- export pure circuit _calculateSignerId(
239
- pk: Secp256k1Point,
240
- salt: Bytes<32>
241
- ): Bytes<32> {
242
- return Secp256k1EcdsaMultisig_calculateSignerId(pk, salt);
243
- }
244
-
245
- /**
246
- * @description Returns the nonce the next `execute` digest will bind to.
247
- *
248
- * @constraints k=6, rows=34 (zkir-v3)
249
- *
250
- * @returns {Uint<64>} The current nonce.
251
- */
252
- export circuit getNonce(): Uint<64> {
253
- return _nonce;
254
- }
255
-
256
- /**
257
- * @description Returns the number of registered signers.
258
- *
259
- * @constraints k=6, rows=34 (zkir-v3)
260
- *
261
- * @returns {Uint<8>} The signer count.
262
- */
263
- export circuit getSignerCount(): Uint<8> {
264
- return Secp256k1EcdsaMultisig_getSignerCount();
265
- }
266
-
267
- /**
268
- * @description Returns the approval threshold.
269
- *
270
- * @constraints k=6, rows=34 (zkir-v3)
271
- *
272
- * @returns {Uint<8>} The threshold.
273
- */
274
- export circuit getThreshold(): Uint<8> {
275
- return Secp256k1EcdsaMultisig_getThreshold();
276
- }
277
-
278
- /**
279
- * @description Returns whether the given commitment is a registered signer.
280
- *
281
- * @constraints k=8, rows=129 (zkir-v3)
282
- *
283
- * @param {Bytes<32>} commitment - The signer commitment to check.
284
- * @returns {Boolean} True if registered.
285
- */
286
- export circuit isSigner(commitment: Bytes<32>): Boolean {
287
- return Secp256k1EcdsaMultisig_isSigner(commitment);
288
- }
289
- }