@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha

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 (33) hide show
  1. package/README.md +3 -3
  2. package/access/AccessControl.compact +3 -35
  3. package/access/Ownable.compact +5 -51
  4. package/access/ShieldedAccessControl.compact +2 -2
  5. package/access/ZOwnablePK.compact +2 -2
  6. package/crypto/ElGamal.compact +473 -0
  7. package/multisig/ForwarderPrivate.compact +229 -0
  8. package/multisig/ForwarderShielded.compact +149 -0
  9. package/multisig/ForwarderUnshielded.compact +152 -0
  10. package/multisig/ProposalManager.compact +328 -0
  11. package/multisig/ShieldedTreasury.compact +201 -0
  12. package/multisig/ShieldedTreasuryStateless.compact +76 -0
  13. package/multisig/Signer.compact +312 -0
  14. package/multisig/SignerManager.compact +205 -0
  15. package/multisig/UnshieldedTreasury.compact +114 -0
  16. package/multisig/presets/ShieldedMultiSig.compact +236 -0
  17. package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
  18. package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
  19. package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
  20. package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
  21. package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
  22. package/package.json +9 -5
  23. package/security/Allowlist.compact +101 -0
  24. package/security/Blocklist.compact +99 -0
  25. package/security/Initializable.compact +2 -2
  26. package/security/Pausable.compact +2 -2
  27. package/token/FungibleToken.compact +24 -82
  28. package/token/MultiToken.compact +21 -80
  29. package/token/NativeShieldedToken.compact +285 -0
  30. package/token/NativeShieldedTokenCore.compact +343 -0
  31. package/token/NativeShieldedTokenFamily.compact +287 -0
  32. package/token/NonFungibleToken.compact +25 -83
  33. package/utils/Utils.compact +86 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.29.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/minokawa-0-18-26-0)
1
+ [![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.31.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/minokawa-0-18-26-0)
2
2
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
3
3
 
4
4
  This project is built on the Midnight Network.
@@ -37,8 +37,8 @@ Follow Midnight's [Compact Developer Tools installation guide](https://docs.midn
37
37
  ```bash
38
38
  $ compact compile --version
39
39
 
40
- Compactc version: 0.29.0
41
- 0.29.0
40
+ Compactc version: 0.31.0
41
+ 0.31.0
42
42
  ```
43
43
 
44
44
  ### Installation
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (access/AccessControl.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module AccessControl
@@ -442,38 +442,6 @@ module AccessControl {
442
442
  * @returns {Bytes<32>} accountId - The computed account identifier.
443
443
  */
444
444
  circuit _computeAccountId(): Bytes<32> {
445
- return computeAccountId(wit_AccessControlSK());
446
- }
447
-
448
- /**
449
- * @description Computes an account identifier without on-chain state, allowing a user to derive
450
- * their identity commitment before submitting it in a grant or revoke operation.
451
- * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
452
- * given the same inputs.
453
- *
454
- * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
455
- * permanently compromise the privacy guarantees of this system:
456
- *
457
- * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
458
- * application logs, analytics pipelines, or any observable side-channel.
459
- * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
460
- * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
461
- * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
462
- * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
463
- * - **Treat key loss as identity loss** — a lost key cannot be recovered. Back up
464
- * keys securely before using them in role commitments.
465
- * - **Avoid calling this circuit in untrusted environments** — executing this in an
466
- * unverified browser extension, compromised runtime, or shared machine may expose
467
- * the key to a malicious observer.
468
- *
469
- * ## ID Derivation
470
- * `accountId = persistentHash(secretKey)`
471
- *
472
- * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
473
- *
474
- * @returns {Bytes<32>} accountId - The computed account identifier.
475
- */
476
- export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
477
- return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
445
+ return Utils_computeAccountId(wit_AccessControlSK());
478
446
  }
479
447
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (access/Ownable.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module Ownable
@@ -125,7 +125,7 @@ module Ownable {
125
125
  export circuit initialize(initialOwner: Either<Bytes<32>, ContractAddress>): [] {
126
126
  assertNotInitialized();
127
127
  _isInitialized = true;
128
- assert(!_isTargetZero(initialOwner), "Ownable: invalid initial owner");
128
+ assert(!Utils_isTargetZero(initialOwner), "Ownable: invalid initial owner");
129
129
  _transferOwnership(initialOwner);
130
130
  }
131
131
 
@@ -219,7 +219,7 @@ module Ownable {
219
219
  export circuit _unsafeTransferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
220
220
  assertInitialized();
221
221
  assertOnlyOwner();
222
- assert(!_isTargetZero(newOwner), "Ownable: invalid new owner");
222
+ assert(!Utils_isTargetZero(newOwner), "Ownable: invalid new owner");
223
223
  _unsafeUncheckedTransferOwnership(newOwner);
224
224
  }
225
225
 
@@ -335,52 +335,6 @@ module Ownable {
335
335
  * @returns {Bytes<32>} accountId - The computed account identifier.
336
336
  */
337
337
  circuit _computeAccountId(): Bytes<32> {
338
- return computeAccountId(wit_OwnableSK());
339
- }
340
-
341
- /**
342
- * @description Computes an account identifier without on-chain state, allowing a user to derive
343
- * their identity commitment before submitting it in an ownership transfer.
344
- * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
345
- * given the same inputs.
346
- *
347
- * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
348
- * permanently compromise the security of this system:
349
- *
350
- * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
351
- * application logs, analytics pipelines, or any observable side-channel.
352
- * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
353
- * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
354
- * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
355
- * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
356
- * - **Treat key loss as identity loss** — a lost key cannot be recovered.
357
- * - **Avoid calling this circuit in untrusted environments** — executing this in an
358
- * unverified browser extension, compromised runtime, or shared machine may expose
359
- * the key to a malicious observer.
360
- *
361
- * ## ID Derivation
362
- * `accountId = persistentHash(secretKey)`
363
- *
364
- * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
365
- *
366
- * @returns {Bytes<32>} accountId - The computed account identifier.
367
- */
368
- export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
369
- return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
370
- }
371
-
372
- /**
373
- * @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
374
- * holds the zero value.
375
- *
376
- * @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
377
- * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
378
- */
379
- circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
380
- if (target.is_left) {
381
- return target.left == default<Bytes<32>>;
382
- } else {
383
- return target.right == default<ContractAddress>;
384
- }
338
+ return Utils_computeAccountId(wit_OwnableSK());
385
339
  }
386
340
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (access/ShieldedAccessControl.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module ShieldedAccessControl
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.2.0 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (access/ZOwnablePK.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module ZOwnablePK
@@ -0,0 +1,473 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (crypto/ElGamal.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ElGamal
8
+ * @description Stateless, single-receiver ElGamal primitives over the Jubjub
9
+ * curve. Provides general point encryption (`encryptPoint` / `assertDecryptsToPoint`)
10
+ * and the exponential (lifted) variant (`encrypt` / `assertDecryptsTo`) for
11
+ * bounded scalar values.
12
+ *
13
+ * In the lifted variant a value `v` is encrypted as the point `g^v`, so the
14
+ * scheme is additively homomorphic in the exponent: ciphertexts of `a` and `b`
15
+ * combine into a ciphertext of `a + b`. Recovering `v` from `g^v` requires a
16
+ * discrete-log search, so the lifted variant is only usable when `v` is bounded;
17
+ * the caller is responsible for enforcing that bound (e.g. capping each encrypted
18
+ * value to a range a discrete-log search can clear). The general point form
19
+ * carries an arbitrary message point and sidesteps the bound, at the cost of
20
+ * decryption recovering only a point (not a scalar).
21
+ *
22
+ * @notice This module holds NO ledger state and declares NO witnesses. Every
23
+ * circuit takes its keys, plaintexts, and randomness as explicit arguments so
24
+ * the same primitives can back many importing modules (single-receiver today;
25
+ * multi-receiver / auditor variants later) without each importer inheriting a
26
+ * witness interface it may not need. Secrets and cached plaintexts are supplied
27
+ * by the importing module's own witnesses and verified there.
28
+ *
29
+ * @dev Hash-to-scalar discipline. Both `secretToScalar` and `expandRandomness`
30
+ * map bytes to a Jubjub scalar via `degradeToTransient(persistentHash(...))`.
31
+ * `persistentHash` is used because the derived public key is stored on-chain
32
+ * and compared against on every later verification; `degradeToTransient`
33
+ * truncates the Field to 31 bytes so the result is ALWAYS a valid Jubjub
34
+ * scalar. Feeding a raw `persistentHash` output into `ecMulGenerator` would
35
+ * occasionally exceed the Jubjub scalar field order and fault at runtime.
36
+ *
37
+ * @dev TRUST ASSUMPTION — subgroup membership. This is the module's most
38
+ * load-bearing assumption. Every `JubjubPoint` reaching a curve operation is
39
+ * assumed to be in the Jubjub prime-order subgroup, including points supplied by
40
+ * a caller as a witness or argument (e.g. a recipient `pk`, or the message point
41
+ * in `encryptPoint`). The Midnight runtime enforces this at the ZK-constraint
42
+ * level: the embedded-curve gadget assigns points via cofactor clearing, so an
43
+ * off-curve / low-order / mixed-order point makes the circuit unsatisfiable.
44
+ * The module therefore performs NO in-circuit subgroup check: the runtime
45
+ * already constrains membership (so it is unnecessary), and the direct test
46
+ * `ℓ*P == O` is unavailable anyway because `ecMul` by `ℓ` faults (verified — `ℓ`
47
+ * exceeds the valid scalar range, while `ℓ-1` is accepted). The `ecNeg` identity
48
+ * (`(ℓ-1)*P = -P`) and the homomorphic algebra depend on this holding.
49
+ *
50
+ * Basis (and its limits): source-verified against `midnight-circuits` 6.0.0 —
51
+ * the version vendored by the Midnight onchain-runtime we compile against —
52
+ * specifically `ecc::native::edwards_chip`, where `assign` cofactor-clears the
53
+ * point and `q_mem`'s `create_membership_gate` enforces on-curve, reached from
54
+ * the ZKIR `ec_mul`/`ec_add` lowering via `ecc_from_parts`. This is NOT a
55
+ * team-confirmed or audited guarantee: compactc and midnight-ledger are
56
+ * unaudited, so treat it as a substantiated-but-unaudited assumption. It is
57
+ * pinned against regression by `crypto/test/CurveRuntimeInvariants.test.ts` (a
58
+ * runtime behavior canary) and MUST be re-verified in source on any Midnight
59
+ * runtime / circuits version bump.
60
+ *
61
+ * @dev Weak keys. Encrypting under the identity point yields `c2 = m` with no
62
+ * masking (the plaintext is exposed), and the identity is a valid subgroup
63
+ * point so the runtime subgroup guarantee does NOT catch it. `encryptPoint`
64
+ * therefore asserts `pk != identity`, and every encryption path routes through
65
+ * it, so confidentiality is protected regardless of the importer. An importing
66
+ * module MAY additionally reject an identity `pk` where keys enter the system
67
+ * (e.g. at registration) to fail fast with a clear error rather than at first
68
+ * encryption — a UX nicety, not a confidentiality requirement.
69
+ *
70
+ * @dev Randomness. `encryptPoint` asserts `r != 0` — like `pk = identity`,
71
+ * `r = 0` zeros the mask (`r*pk = O`), yielding the non-hiding `(O, m)`; it is
72
+ * the unique mask-zeroing randomness, so the two asserts together guarantee a
73
+ * non-trivial mask. The remaining obligations the circuit CANNOT enforce:
74
+ * every `r` MUST be a valid Jubjub scalar (`< ℓ`, the subgroup order; larger
75
+ * values fault `ecMul`), and MUST be fresh and secret. Reusing an `r` under the
76
+ * same `pk` leaks the difference of the two plaintexts (`c1` collides, so
77
+ * `c2 - c2' = m - m'`). Prefer `expandRandomness` with a distinct tag per
78
+ * encryption.
79
+ */
80
+ module ElGamal {
81
+ import CompactStandardLibrary;
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Types
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * @description Single-receiver ElGamal ciphertext over Jubjub.
89
+ * c1 = g^r
90
+ * c2 = pk^r * m (lifted: m = g^v)
91
+ */
92
+ export struct Ciphertext {
93
+ c1: JubjubPoint;
94
+ c2: JubjubPoint;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Key / scalar derivation
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * @description Maps a 32-byte secret to a valid Jubjub scalar. See the
103
+ * module-level hash-to-scalar note for why `degradeToTransient` is required.
104
+ *
105
+ * @param secret - The 32-byte secret to map.
106
+ * @return A Field guaranteed to be a valid Jubjub scalar.
107
+ */
108
+ export pure circuit secretToScalar(secret: Bytes<32>): Field {
109
+ return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret]));
110
+ }
111
+
112
+ /**
113
+ * @description Derives the ElGamal public key `pk = g^secretToScalar(ek)`
114
+ * from a secret `ek`.
115
+ *
116
+ * @param ek - The 32-byte encryption secret.
117
+ * @return The corresponding public key point.
118
+ */
119
+ export pure circuit derivePk(ek: Bytes<32>): JubjubPoint {
120
+ return ecMulGenerator(secretToScalar(ek));
121
+ }
122
+
123
+ /**
124
+ * @description Deterministically expands a 32-byte seed into a Jubjub scalar
125
+ * tagged by a distinguishing string, so a single witness seed can yield many
126
+ * distinct randomness values within one circuit invocation. Distinct tags
127
+ * always produce distinct outputs (collision resistance of the hash); the
128
+ * caller is responsible for using distinct tags.
129
+ *
130
+ * @param seed - The seed to expand.
131
+ * @param tag - A 32-byte tag distinguishing this expansion from its siblings.
132
+ * @return A Field suitable for use as encryption randomness.
133
+ */
134
+ export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>): Field {
135
+ return degradeToTransient(persistentHash<Vector<2, Bytes<32>>>([seed, tag]));
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Curve helpers
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * @description The order of the Jubjub prime-order subgroup (`EmbeddedFr`
144
+ * modulus `ℓ`), minus one. Used as the scalar for point negation: for any `P`
145
+ * in the subgroup, `(ℓ - 1) * P = ℓ*P - P = O - P = -P`.
146
+ *
147
+ * @notice The negation identity this enables (`P + (ℓ-1)*P == O`) is exercised
148
+ * by the `negate` round-trip tests. Compact has no module-level constants, so
149
+ * the value is exposed as a pure circuit returning the literal.
150
+ */
151
+ pure circuit JUBJUB_SUBGROUP_ORDER_MINUS_ONE(): Field {
152
+ return 6554484396890773809930967563523245729705921265872317281365359162392183254198 as Field;
153
+ }
154
+
155
+ /**
156
+ * @description Negates a Jubjub point.
157
+ *
158
+ * @notice Implemented as scalar multiplication by `ℓ-1` (via
159
+ * `JUBJUB_SUBGROUP_ORDER_MINUS_ONE`) rather than the obvious `ecMul(p, -1)`:
160
+ * the scalar `-1` computed in the `Field` type is `BLS_modulus - 1`, which
161
+ * exceeds the Jubjub scalar field order and faults `ecMul` ("failed to decode
162
+ * for built-in type EmbeddedFr"). (Coordinate negation `(-x, y)` via
163
+ * `constructJubjubPoint`/`jubjubPointX`/`jubjubPointY` is an equivalent
164
+ * alternative that does not depend on the subgroup assumption.) The
165
+ * `(ℓ-1)*P = -P` identity holds only in the prime-order subgroup, but that
166
+ * always applies here: the runtime constrains every assigned `JubjubPoint`
167
+ * into the prime-order subgroup (the embedded-curve gadget assigns points via
168
+ * cofactor clearing), so no in-subgroup precondition needs to be checked or
169
+ * assumed. See the module-level subgroup note.
170
+ */
171
+ pure circuit ecNeg(p: JubjubPoint): JubjubPoint {
172
+ return ecMul(p, JUBJUB_SUBGROUP_ORDER_MINUS_ONE());
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Encryption & homomorphic operations
177
+ // ---------------------------------------------------------------------------
178
+
179
+ /**
180
+ * @description The identity ciphertext `Enc(0)` (both components are the
181
+ * curve identity). Useful for initializing a fresh balance.
182
+ *
183
+ * @notice This is a fixed, deterministic value `(O, O)` — NOT a hiding
184
+ * encryption. It is publicly recognizable, so a slot left at `encryptZero` is
185
+ * distinguishable as never-written. Rerandomize (or credit with fresh `r`)
186
+ * before treating it as private.
187
+ */
188
+ export pure circuit encryptZero(): Ciphertext {
189
+ const id = ecMulGenerator(0 as Field);
190
+ return Ciphertext { c1: id, c2: id };
191
+ }
192
+
193
+ /**
194
+ * @description Encrypts an arbitrary message point `m` under `pk` with
195
+ * randomness `r`, producing the ciphertext `(g^r, pk^r * m)`. This is the
196
+ * general (non-lifted) ElGamal encryption; `encrypt` is the lifted special
197
+ * case `encryptPoint(pk, g^value, r)`.
198
+ *
199
+ * @notice `m` (like `pk`) is constrained into the prime-order subgroup by the
200
+ * runtime (see the module-level subgroup note), so no in-circuit membership
201
+ * check is performed. `pk` is asserted non-identity and `r` non-zero — the
202
+ * two inputs that would zero the mask and expose `m` (see the weak-keys and
203
+ * randomness notes). Decrypting `m` is only feasible when it is a known or
204
+ * small-exponent point; the caller owns that concern (the lifted `encrypt`
205
+ * bounds it via the discrete-log range).
206
+ *
207
+ * Requirements:
208
+ *
209
+ * - `pk` is not the identity point.
210
+ * - `r` is non-zero.
211
+ *
212
+ * @param pk - The recipient public key.
213
+ * @param m - The message point to encrypt.
214
+ * @param r - The encryption randomness.
215
+ * @return The ciphertext `(g^r, pk^r * m)`.
216
+ */
217
+ export pure circuit encryptPoint(pk: JubjubPoint, m: JubjubPoint, r: Field): Ciphertext {
218
+ assert(pk != ecMulGenerator(0 as Field), "ElGamal: identity pk");
219
+ assert(r != 0 as Field, "ElGamal: zero randomness");
220
+ const c1 = ecMulGenerator(r);
221
+ const mask = ecMul(pk, r);
222
+ const c2 = ecAdd(mask, m);
223
+ return Ciphertext { c1: c1, c2: c2 };
224
+ }
225
+
226
+ /**
227
+ * @description Produces a fresh ciphertext of `value` under `pk` with
228
+ * randomness `r`, lifting `value` to the point `g^value`. Randomness is taken
229
+ * as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion
230
+ * at every call site; `expandRandomness` produces `Field` for the same reason.
231
+ *
232
+ * Requirements:
233
+ *
234
+ * - `pk` is not the identity point.
235
+ * - `r` is non-zero.
236
+ *
237
+ * @param pk - The recipient public key.
238
+ * @param value - The plaintext value to encrypt.
239
+ * @param r - The encryption randomness.
240
+ * @return The ciphertext `(g^r, pk^r * g^value)`.
241
+ */
242
+ export pure circuit encrypt(pk: JubjubPoint, value: Uint<128>, r: Field): Ciphertext {
243
+ return encryptPoint(pk, ecMulGenerator(value as Field), r);
244
+ }
245
+
246
+ /**
247
+ * @description Negates a ciphertext componentwise: if `ct` encrypts `v` then
248
+ * the result encrypts `-v` under the same key (with negated randomness).
249
+ * This is the building block `sub` rests on.
250
+ *
251
+ * @param ct - The ciphertext to negate.
252
+ * @return A ciphertext of `-v`.
253
+ */
254
+ export pure circuit negate(ct: Ciphertext): Ciphertext {
255
+ return Ciphertext { c1: ecNeg(ct.c1), c2: ecNeg(ct.c2) };
256
+ }
257
+
258
+ /**
259
+ * @description Homomorphically adds two ciphertexts: `Enc(a) + Enc(b)`
260
+ * decrypts to `a + b`. This is the defining additive-homomorphism primitive
261
+ * and combines two independently produced ciphertexts componentwise.
262
+ *
263
+ * @notice Both inputs MUST be encrypted under the same public key for the
264
+ * result to decrypt under that key's secret; combining ciphertexts under
265
+ * different keys yields a ciphertext no single key can open. The randomness
266
+ * of the result is the sum of the inputs' randomness, so the result is a
267
+ * valid fresh ciphertext under that key.
268
+ *
269
+ * @param a - The first ciphertext.
270
+ * @param b - The second ciphertext (same recipient key as `a`).
271
+ * @return A ciphertext of `a + b`.
272
+ */
273
+ export pure circuit add(a: Ciphertext, b: Ciphertext): Ciphertext {
274
+ return Ciphertext { c1: ecAdd(a.c1, b.c1), c2: ecAdd(a.c2, b.c2) };
275
+ }
276
+
277
+ /**
278
+ * @description Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)`
279
+ * decrypts to `a - b`. Implemented as `add(a, negate(b))`.
280
+ *
281
+ * @notice Same-key requirement as `add`. The caller MUST ensure the
282
+ * underlying plaintext subtraction does not underflow; this circuit cannot
283
+ * check it (see `subEncrypted`).
284
+ *
285
+ * @param a - The minuend ciphertext.
286
+ * @param b - The subtrahend ciphertext (same recipient key as `a`).
287
+ * @return A ciphertext of `a - b`.
288
+ */
289
+ export pure circuit sub(a: Ciphertext, b: Ciphertext): Ciphertext {
290
+ return add(a, negate(b));
291
+ }
292
+
293
+ /**
294
+ * @description Homomorphically multiplies the plaintext of `ct` by a public
295
+ * scalar `k`: if `ct` encrypts `v` then the result encrypts `k * v` under the
296
+ * same key. Both components are scaled by `k`, so the randomness scales to
297
+ * `k * r` and the result is a valid fresh ciphertext under that key.
298
+ *
299
+ * This is the scalar-multiplication leg of the homomorphism, and the only way
300
+ * to express linear combinations: a weighted sum `Enc(3a + 5b)` is
301
+ * `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated `add` cannot do
302
+ * for large weights.
303
+ *
304
+ * @notice `k` MUST be a valid Jubjub scalar (less than the subgroup order);
305
+ * larger values fault `ecMul` (see the module-level hash-to-scalar note). The
306
+ * caller also remains responsible for the discrete-log bound — `k * v` must
307
+ * stay within the range a recovery search can clear.
308
+ *
309
+ * @param ct - The ciphertext whose plaintext is scaled.
310
+ * @param k - The public scalar multiplier (a valid Jubjub scalar).
311
+ * @return A ciphertext of `k * v`.
312
+ */
313
+ export pure circuit scalarMul(ct: Ciphertext, k: Field): Ciphertext {
314
+ return Ciphertext { c1: ecMul(ct.c1, k), c2: ecMul(ct.c2, k) };
315
+ }
316
+
317
+ /**
318
+ * @description Homomorphically adds `value` to the plaintext encrypted by
319
+ * `old`, producing a fresh ciphertext under `pk` with new randomness `r`.
320
+ * Convenience over `add` for folding a known plaintext into a ciphertext:
321
+ * `add(old, encrypt(pk, value, r))`.
322
+ *
323
+ * Requirements:
324
+ *
325
+ * - `pk` is not the identity point.
326
+ * - `r` is non-zero.
327
+ */
328
+ export pure circuit addEncrypted(
329
+ old: Ciphertext,
330
+ pk: JubjubPoint,
331
+ value: Uint<128>,
332
+ r: Field
333
+ ): Ciphertext {
334
+ return add(old, encrypt(pk, value, r));
335
+ }
336
+
337
+ /**
338
+ * @description Homomorphically subtracts `value` from the plaintext encrypted
339
+ * by `old`. Convenience over `sub`: `sub(old, encrypt(pk, value, r))`.
340
+ *
341
+ * @notice The caller MUST ensure the underlying plaintext subtraction does
342
+ * not underflow; this circuit cannot check it. Callers verify sufficiency by
343
+ * reading the plaintext via their own witness and asserting it with
344
+ * `assertDecryptsTo` before calling.
345
+ *
346
+ * Requirements:
347
+ *
348
+ * - `pk` is not the identity point.
349
+ * - `r` is non-zero.
350
+ */
351
+ export pure circuit subEncrypted(
352
+ old: Ciphertext,
353
+ pk: JubjubPoint,
354
+ value: Uint<128>,
355
+ r: Field
356
+ ): Ciphertext {
357
+ return sub(old, encrypt(pk, value, r));
358
+ }
359
+
360
+ /**
361
+ * @description Rerandomizes `ct`: returns a fresh ciphertext of the same
362
+ * plaintext under the same key `pk`, using new randomness `r`. The output is
363
+ * unlinkable to the input (it is a uniformly fresh encryption of the same
364
+ * value) yet decrypts identically. Equivalent to homomorphically adding an
365
+ * encryption of zero.
366
+ *
367
+ * @notice `r = 0` is rejected (it would be a no-op via the `encryptPoint`
368
+ * zero-randomness assert). One silent failure mode remains that the circuit
369
+ * cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts
370
+ * the plaintext (the result no longer decrypts to the original value). Pass
371
+ * fresh randomness and the matching `pk`.
372
+ *
373
+ * Requirements:
374
+ *
375
+ * - `pk` is not the identity point.
376
+ * - `r` is non-zero.
377
+ *
378
+ * @param ct - The ciphertext to rerandomize.
379
+ * @param pk - The recipient public key `ct` is encrypted under.
380
+ * @param r - Fresh, nonzero randomness for the rerandomization.
381
+ * @return A fresh ciphertext of the same plaintext.
382
+ */
383
+ export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: Field): Ciphertext {
384
+ return add(ct, encrypt(pk, 0, r));
385
+ }
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // Verification
389
+ // ---------------------------------------------------------------------------
390
+
391
+ /**
392
+ * @description Asserts that `ek` is the secret for `pk`, i.e.
393
+ * `derivePk(ek) == pk`. Lets a caller prove ownership of a public key
394
+ * without touching any ciphertext or plaintext.
395
+ *
396
+ * Requirements:
397
+ *
398
+ * - `ek` is the secret for `pk` (`derivePk(ek) == pk`).
399
+ *
400
+ * @param pk - The public key to check.
401
+ * @param ek - The secret claimed to correspond to `pk`.
402
+ */
403
+ export pure circuit assertKeyPair(pk: JubjubPoint, ek: Bytes<32>): [] {
404
+ assert(derivePk(ek) == pk,
405
+ "ElGamal: ek/pk mismatch");
406
+ }
407
+
408
+ /**
409
+ * @description Asserts that `ct` decrypts under `(pk, ek)` to the message
410
+ * point `m`, and that `ek` is the secret for `pk`. This is the general
411
+ * (non-lifted) verification; `assertDecryptsTo` is the lifted special case
412
+ * with `m = g^claimedValue`.
413
+ *
414
+ * Requirements:
415
+ *
416
+ * - `ek` is the secret for `pk`.
417
+ * - `ct` decrypts under `(pk, ek)` to `m`.
418
+ *
419
+ * @param ct - The ciphertext to verify.
420
+ * @param pk - The public key `ct` is expected to be encrypted under.
421
+ * @param ek - The secret claimed to correspond to `pk`.
422
+ * @param m - The message point `ct` is claimed to encrypt.
423
+ */
424
+ export pure circuit assertDecryptsToPoint(
425
+ ct: Ciphertext,
426
+ pk: JubjubPoint,
427
+ ek: Bytes<32>,
428
+ m: JubjubPoint
429
+ ): [] {
430
+ // Inlines the same key check as `assertKeyPair` (rather than calling it) so
431
+ // `ekField` is computed once and reused for the decryption below, avoiding
432
+ // a second `secretToScalar` hash in this hot verification path.
433
+ const ekField = secretToScalar(ek);
434
+ assert(ecMulGenerator(ekField) == pk,
435
+ "ElGamal: ek/pk mismatch");
436
+
437
+ // m = c2 - ek*c1. ek*c1 is negated via ecNeg (scalar mul by ℓ-1) rather
438
+ // than ecMul by a negative scalar, which would exceed the Jubjub scalar
439
+ // field. ekField < ℓ (the subgroup order), so ecMul(ct.c1, ekField) is a
440
+ // valid scalar mul.
441
+ const ekC1 = ecMul(ct.c1, ekField);
442
+ const mPoint = ecAdd(ct.c2, ecNeg(ekC1));
443
+ assert(mPoint == m,
444
+ "ElGamal: plaintext mismatch");
445
+ }
446
+
447
+ /**
448
+ * @description Asserts that `ct` decrypts under `(pk, ek)` to `claimedValue`,
449
+ * and that `ek` is the secret for `pk`. This is the in-circuit verification
450
+ * path an importing module uses to bind its witness-supplied `ek` and cached
451
+ * plaintext to publicly stored state: a wrong `ek` fails the key check, a
452
+ * wrong plaintext fails the decryption check. Lifted special case of
453
+ * `assertDecryptsToPoint` with the message point `g^claimedValue`.
454
+ *
455
+ * Requirements:
456
+ *
457
+ * - `ek` is the secret for `pk`.
458
+ * - `ct` decrypts under `(pk, ek)` to `claimedValue`.
459
+ *
460
+ * @param ct - The ciphertext to verify.
461
+ * @param pk - The public key `ct` is expected to be encrypted under.
462
+ * @param ek - The secret claimed to correspond to `pk`.
463
+ * @param claimedValue - The plaintext `ct` is claimed to encrypt.
464
+ */
465
+ export pure circuit assertDecryptsTo(
466
+ ct: Ciphertext,
467
+ pk: JubjubPoint,
468
+ ek: Bytes<32>,
469
+ claimedValue: Uint<128>
470
+ ): [] {
471
+ assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as Field));
472
+ }
473
+ }