@openzeppelin/compact-contracts 0.1.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 +15 -16
  2. package/access/AccessControl.compact +3 -35
  3. package/access/Ownable.compact +47 -60
  4. package/access/ShieldedAccessControl.compact +43 -10
  5. package/access/ZOwnablePK.compact +45 -10
  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 +12 -6
  23. package/security/Allowlist.compact +101 -0
  24. package/security/Blocklist.compact +99 -0
  25. package/security/Initializable.compact +18 -2
  26. package/security/Pausable.compact +2 -2
  27. package/token/FungibleToken.compact +79 -103
  28. package/token/MultiToken.compact +67 -93
  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 +85 -110
  33. package/utils/Utils.compact +86 -2
@@ -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
+ }