@openzeppelin/compact-contracts 0.4.0-alpha.6 → 0.5.0-alpha.1

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 (43) hide show
  1. package/access/AccessControl.compact +1 -1
  2. package/access/Ownable.compact +1 -1
  3. package/access/ShieldedAccessControl.compact +30 -26
  4. package/access/ZOwnablePK.compact +1 -1
  5. package/crypto/Ecdh.compact +153 -0
  6. package/crypto/EcdhMask.compact +103 -70
  7. package/crypto/Ecdsa.compact +1 -1
  8. package/crypto/Eip712.compact +1 -1
  9. package/crypto/ElGamal.compact +1 -1
  10. package/multisig/ForwarderPrivate.compact +1 -1
  11. package/multisig/ForwarderShielded.compact +1 -1
  12. package/multisig/ForwarderUnshielded.compact +1 -1
  13. package/multisig/ProposalManager.compact +1 -1
  14. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  15. package/multisig/ShieldedTreasury.compact +1 -1
  16. package/multisig/ShieldedTreasuryStateless.compact +1 -1
  17. package/multisig/Signer.compact +1 -1
  18. package/multisig/UnshieldedTreasury.compact +1 -1
  19. package/multisig/examples/ForwarderPrivateExample.compact +1 -1
  20. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  21. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +1 -1
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +1 -1
  25. package/multisig/presets/ShieldedMultiSigV2.compact +1 -1
  26. package/package.json +1 -1
  27. package/security/Allowlist.compact +1 -1
  28. package/security/Blocklist.compact +1 -1
  29. package/security/Initializable.compact +1 -1
  30. package/security/Pausable.compact +1 -1
  31. package/token/ConfidentialFungibleToken.compact +1 -1
  32. package/token/ConfidentialNoteFungibleToken.compact +741 -0
  33. package/token/FungibleToken.compact +1 -1
  34. package/token/MultiToken.compact +1 -1
  35. package/token/NativeShieldedToken.compact +1 -1
  36. package/token/NativeShieldedTokenCore.compact +1 -1
  37. package/token/NativeShieldedTokenFamily.compact +1 -1
  38. package/token/NonFungibleToken.compact +1 -1
  39. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  40. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  41. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  42. package/utils/EvmAbi.compact +1 -1
  43. package/utils/Utils.compact +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (access/AccessControl.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.6 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (access/Ownable.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.6 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (access/ShieldedAccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -108,8 +108,8 @@ pragma language_version >= 0.26.0;
108
108
  * depends on the number of plausible `(role, accountId)` pairings. Since role identifiers
109
109
  * are public, the anonymity set is effectively the number of plausible account IDs per role.
110
110
  * - Outside observers will know when roles are granted and revoked.
111
- * - Outside observers can infer the total number of role grants made across all roles—not per-role counts,
112
- * but the cumulative total.
111
+ * - Outside observers can infer the total number of distinct `(role, accountId)` pairings granted
112
+ * across all roles, not per-role counts.
113
113
  * - Outside observers can link calls made by the same role instance across time.
114
114
  * - Users can be retroactively deanonymized if their secret key is exposed.
115
115
  * - Outside observers will NOT be able to identify the holder of any role
@@ -133,12 +133,8 @@ pragma language_version >= 0.26.0;
133
133
  * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`.
134
134
  * Failure to do so may weaken privacy guarantees.
135
135
  * - The `_instanceSalt` is immutable and used to differentiate deployments.
136
- * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
137
- * Deployers should monitor slot consumption off-chain. A careless admin can exhaust
138
- * capacity through repeated grants of the same active (role, accountId) pairing.
139
- * Duplicate grants are benign from a security perspective. They produce identical
140
- * commitments and a single nullifier invalidates all copies; however, they waste tree capacity.
141
- * Implementing contracts are responsible for mitigating tree exhaustion risk.
136
+ * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. Each distinct
137
+ * `(role, accountId)` pairing consumes one slot; re-granting an active pairing is a no-op.
142
138
  * - A single `secretKey` compromise exposes all roles held by that user within the contract
143
139
  * instance, since all roles share the same `accountId`. Users requiring compartmentalization
144
140
  * between roles should use separate keys per role and accept the additional key management overhead.
@@ -155,7 +151,8 @@ pragma language_version >= 0.26.0;
155
151
  * - The Merkle tree capacity of 2^20 leaf slots is a permanent, irrecoverable resource.
156
152
  * There is no mechanism to reclaim slots after revocation, no upgrade path, and no emergency
157
153
  * recovery. Once exhausted, the contract is permanently unable to grant new roles. Deployers
158
- * must monitor slot consumption off-chain and plan capacity accordingly.
154
+ * must monitor slot consumption off-chain and plan capacity accordingly. The exported tree
155
+ * reports occupancy via `firstFree()` (TypeScript) and `isFull()` (circuit).
159
156
  *
160
157
  * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we
161
158
  * plan on migrating to a ZK-friendly hashing function when an implementation is available.
@@ -213,6 +210,13 @@ module ShieldedAccessControl {
213
210
  */
214
211
  export ledger _roleCommitmentNullifiers: Set<RoleNullifier>;
215
212
 
213
+ /**
214
+ * @description Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so
215
+ * one pairing can never consume more than one `_operatorRoles` slot.
216
+ * @type {Set<RoleNullifier>} _grantedRoleNullifiers
217
+ */
218
+ export ledger _grantedRoleNullifiers: Set<RoleNullifier>;
219
+
216
220
  /**
217
221
  * @sealed @ledger _instanceSalt
218
222
  * @description A per-instance value provided at initialization used to namespace
@@ -395,12 +399,11 @@ module ShieldedAccessControl {
395
399
 
396
400
  /**
397
401
  * @description Grants `role` to `accountId` by inserting a role commitment unique to the
398
- * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
399
- * so long as they remain unrevoked. This does not yield any additional authority and simply wastes
400
- * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
402
+ * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
403
+ * pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
401
404
  * `accountId` must be generated to be re-authorized for a revoked `role`.
402
405
  *
403
- * @constraints k=15, rows=28995
406
+ * @constraints k=15, rows=29071
404
407
  *
405
408
  * Requirements:
406
409
  *
@@ -429,9 +432,8 @@ module ShieldedAccessControl {
429
432
 
430
433
  /**
431
434
  * @description Grants `role` to `accountId` by inserting a role commitment unique to the
432
- * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
433
- * so long as they remain unrevoked. This does not yield any additional authority and simply wastes
434
- * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
435
+ * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
436
+ * pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
435
437
  * `accountId` must be generated to be re-authorized for a revoked `role`.
436
438
  *
437
439
  * Internal circuit without access restriction.
@@ -439,12 +441,10 @@ module ShieldedAccessControl {
439
441
  * @warning Exposing this circuit directly in an implementing contract would allow anyone to grant
440
442
  * roles without authorization. It must be wrapped with appropriate access control.
441
443
  *
442
- * @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
443
- * Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity
444
- * but are otherwise benign. A single nullifier invalidates all duplicate commitments.
445
- * Implementing contracts are responsible for mitigating tree exhaustion risk.
444
+ * @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per
445
+ * distinct pairing. Deployers should monitor slot consumption off-chain.
446
446
  *
447
- * @constraints k=14, rows=12250
447
+ * @constraints k=14, rows=12326
448
448
  *
449
449
  * Requirements:
450
450
  *
@@ -573,13 +573,14 @@ module ShieldedAccessControl {
573
573
  /**
574
574
  * @description Core business logic for the grant/revoke role circuits. Asserts that the
575
575
  * `(role, accountId)` pairing has not already been revoked. On success, dispatches on
576
- * `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and
577
- * a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`.
576
+ * `updateType`: a `Grant` inserts the role commitment into `_operatorRoles` unless the
577
+ * nullifier is already in `_grantedRoleNullifiers`, and a `Revoke` inserts the nullifier
578
+ * into `_roleCommitmentNullifiers`.
578
579
  *
579
580
  * Disclosures:
580
581
  *
581
582
  * - A nullifier for the respective role commitment.
582
- * - A role commitment (on Grant only).
583
+ * - A role commitment (on first Grant only).
583
584
  *
584
585
  * @notice The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call,
585
586
  * regardless of the update type. Since `Set.member` publicly reveals its argument, the
@@ -603,7 +604,10 @@ module ShieldedAccessControl {
603
604
  assert(!isRevoked, "ShieldedAccessControl: role is already revoked");
604
605
 
605
606
  if (updateType == UpdateType.Grant) {
606
- _operatorRoles.insert(disclose(roleCommitment));
607
+ if (!_grantedRoleNullifiers.member(disclose(roleNullifier))) {
608
+ _grantedRoleNullifiers.insert(disclose(roleNullifier));
609
+ _operatorRoles.insert(disclose(roleCommitment));
610
+ }
607
611
  } else {
608
612
  _roleCommitmentNullifiers.insert(disclose(roleNullifier));
609
613
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (access/ZOwnablePK.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -0,0 +1,153 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/Ecdh.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module Ecdh
8
+ * @description Stateless Diffie-Hellman key agreement over Jubjub. Splits into
9
+ * the two halves of one exchange: `deriveShared` runs the sender side and
10
+ * returns the ephemeral public key to publish alongside the shared secret, and
11
+ * `recoverShared` runs the recipient side from that published point. The module
12
+ * agrees on a point and nothing more: deriving keys, masks or nonces from it
13
+ * belongs to the consumer (`crypto/EcdhMask` for hybrid encryption).
14
+ *
15
+ * @see https://www.secg.org/sec1-v2.pdf#page=33 SEC 1 v2.0 §3.3.1, Elliptic Curve Diffie-Hellman Primitive
16
+ * @see https://www.secg.org/sec1-v2.pdf#page=30 SEC 1 v2.0 §3.2.2.1, Elliptic Curve Public Key Validation Primitive
17
+ * @see https://www.secg.org/sec1-v2.pdf#page=29 SEC 1 v2.0 §3.2.1, Elliptic Curve Key Pair Generation Primitive
18
+ *
19
+ * @dev Construction and notation. SEC 1's symbol follows in brackets.
20
+ * - `g`: the Jubjub prime-order subgroup generator, of order `ℓ` [`G`, `n`].
21
+ * - `ek`: the recipient's secret scalar in `[1, ℓ-1]` [`d`, §3.2.1].
22
+ * - `e`: the sender's ephemeral scalar in `[1, ℓ-1]`, fresh per call [`d`, §3.2.1].
23
+ * - `pk = g^ek`: the recipient's public key [`Q`, §3.2.1].
24
+ * - `E = g^e`: the ephemeral public key, published [`Q`, §3.2.1].
25
+ * - `S = pk^e = g^{ek*e} = E^ek`: the shared secret point, never published
26
+ * [`P = d * Q`, §3.3.1]. §3.3.1 outputs `z = x_P`; this module returns `P`
27
+ * itself and leaves the KDF to the consumer.
28
+ * - `identity`: the point at infinity, `ecMulGenerator(0)` [`O`]. §3.2.2.1
29
+ * action 1 rejects it as a key; §3.3.1 action 2 rejects it as `P`.
30
+ * `deriveShared` does the first; the second cannot fire in a prime-order
31
+ * group once the first holds and `e ≢ 0 (mod ℓ)`.
32
+ * - Jubjub only. A second curve gets its own module with the same circuit names.
33
+ *
34
+ * @dev Future APIs.
35
+ * - `deriveEphemeral(seed: Bytes<32>, nonce: Field): JubjubScalar` — a canonical
36
+ * `e` for consumers, making the uniqueness half of the freshness rule true by
37
+ * construction rather than a precondition.
38
+ * - `assertValidPk(pk)` / `isIdentity(pk): Boolean` — reject an identity key
39
+ * where it is registered. The `deriveShared` guard fires on the sender's
40
+ * transaction, so a bad key costs the sender, not the registrant.
41
+ * - Jubjub key-pair derivation lives in `crypto/ElGamal` (`derivePk`,
42
+ * `secretToScalar`); a shared key module is its natural home.
43
+ *
44
+ * @dev Freshness and secrecy precondition (CRITICAL). `e` MUST be both unique per
45
+ * call to the same recipient AND secret (unpredictable to observers), even across
46
+ * circuit invocations that reuse a randomness seed.
47
+ *
48
+ * Uniqueness: a repeated `e` to one recipient repeats `S`, so every key, mask or
49
+ * nonce a consumer derives from `S` repeats too. Where that derivation is a
50
+ * one-time pad (`crypto/EcdhMask`) the reuse is catastrophic: subtracting two
51
+ * ciphertexts reveals the plaintext difference, and either plaintext once the
52
+ * other is known or constrained.
53
+ *
54
+ * Secrecy: `recipientPk` is public, so anyone who learns `e` recomputes
55
+ * `S = recipientPk^e` and everything derived from it. A unique but PREDICTABLE
56
+ * `e` (a plain counter, a public nonce) therefore breaks confidentiality
57
+ * completely. `e` must be high-entropy and secret, exactly like an ElGamal
58
+ * encryption randomness (see `crypto/ElGamal`), not merely distinct.
59
+ *
60
+ * The caller supplies `e` and owns both properties, e.g. by expanding a secret
61
+ * per-invocation seed bound to a per-recipient-unique nonce (a monotonic index).
62
+ * This is stronger than exponential ElGamal's uniqueness requirement (there,
63
+ * reuse merely leaks plaintext equality); do not treat `e` as "any randomness."
64
+ *
65
+ * @dev Weak inputs. Two degenerate inputs collapse `S` to the identity point,
66
+ * which every observer can compute: `recipientPk = identity` gives `S = identity`
67
+ * for any `e`, and `e = 0` gives `S = identity` for any key. Anything a consumer
68
+ * derives from such an `S` is public. The identity is a valid subgroup point, so
69
+ * the runtime subgroup guarantee does NOT catch it; `deriveShared` therefore
70
+ * asserts `recipientPk != identity` and `ephemeralPk (= g^e) != identity`, and
71
+ * every sender path reaches both through it. Guarding the ephemeral POINT rather
72
+ * than the scalar subsumes `e != 0` and stays correct regardless of how the
73
+ * runtime treats an out-of-range scalar: `g^e = identity` captures both `e = 0`
74
+ * and any `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the
75
+ * runtime faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that
76
+ * dependency.) These are the only weak inputs (for a subgroup key,
77
+ * `S = identity` iff the ephemeral point is the identity).
78
+ *
79
+ * @dev Subgroup membership. Secrecy of `S` relies on the runtime constraining
80
+ * every `JubjubPoint` into the prime-order subgroup (argued in `crypto/ElGamal`),
81
+ * so a caller cannot hand in a low-order point that leaks `ekScalar`.
82
+ *
83
+ * @dev No ledger state, no witnesses. Both circuits are pure and take their keys
84
+ * and randomness as explicit arguments. `recoverShared` is exposed for
85
+ * wallet/off-chain use (recovering a received value needs no proof); the on-chain
86
+ * path only ever calls `deriveShared`.
87
+ */
88
+ module Ecdh {
89
+ import CompactStandardLibrary;
90
+
91
+ /**
92
+ * @description One sender-side ECDH step: the ephemeral public key to publish
93
+ * and the shared secret point to derive from.
94
+ *
95
+ * @notice `sShared` is secret. Keep it a circuit local; disclosing it opens
96
+ * everything derived from it.
97
+ */
98
+ export struct SharedSecret {
99
+ /** `E = g^e`, public. The recipient recovers `S = E^ek` from it. */
100
+ ephemeralPk: JubjubPoint;
101
+
102
+ /** `S = pk^e`, secret. Never disclose; derive from it in-circuit. */
103
+ sShared: JubjubPoint;
104
+ }
105
+
106
+ /**
107
+ * @description Runs the sender's ECDH step: the ephemeral public key `g^e` to
108
+ * publish and the shared secret `pk^e` to derive from. The single site of the
109
+ * weak-input guards.
110
+ *
111
+ * @notice `e` MUST be fresh per call and secret (see the module freshness
112
+ * precondition).
113
+ *
114
+ * @constraints k=11, rows=1385
115
+ *
116
+ * Requirements:
117
+ *
118
+ * - `recipientPk` is not the identity point.
119
+ * - `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call.
120
+ *
121
+ * @param recipientPk - The recipient's Jubjub public key (`g^ek`).
122
+ * @param e - A fresh ephemeral scalar.
123
+ * @return `{ ephemeralPk = g^e, sShared = recipientPk^e }`.
124
+ */
125
+ export pure circuit deriveShared(recipientPk: JubjubPoint, e: JubjubScalar): SharedSecret {
126
+ const identity = ecMulGenerator(0 as JubjubScalar);
127
+ // An identity key gives S = identity for every `e`.
128
+ assert(recipientPk != identity, "Ecdh: identity pk");
129
+ const ephemeralPk = ecMulGenerator(e);
130
+ // Guarding the POINT subsumes `e == 0` and any `e ≡ 0 (mod ℓ)` (see the
131
+ // weak-inputs note).
132
+ assert(ephemeralPk != identity, "Ecdh: zero ephemeral");
133
+ return SharedSecret { ephemeralPk: ephemeralPk, sShared: ecMul(recipientPk, e) };
134
+ }
135
+
136
+ /**
137
+ * @description The recipient's half of `deriveShared`: recomputes `S = E^ek`
138
+ * from a published ephemeral public key.
139
+ *
140
+ * @dev Never asserts. A wrong `ekScalar` yields an unrelated point, so a wallet
141
+ * scanning exchanges not addressed to it learns nothing from this circuit and
142
+ * the module exposes no key-correctness oracle.
143
+ *
144
+ * @constraints k=9, rows=440
145
+ *
146
+ * @param ephemeralPk - The sender's published `g^e`.
147
+ * @param ekScalar - The recipient's secret scalar.
148
+ * @return The ECDH shared secret point.
149
+ */
150
+ export pure circuit recoverShared(ephemeralPk: JubjubPoint, ekScalar: JubjubScalar): JubjubPoint {
151
+ return ecMul(ephemeralPk, ekScalar);
152
+ }
153
+ }
@@ -1,23 +1,41 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/EcdhMask.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/EcdhMask.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module EcdhMask
8
8
  * @description Stateless ECDH hybrid encryption of a single field element to a
9
- * Jubjub public key. Delivers a value to whoever holds the secret behind a
10
- * Jubjub public key WITHOUT encoding it in the exponent, so recovery needs no
11
- * discrete-log search and the value is unbounded within `Uint<128>`. This is the
12
- * direct-decrypt counterpart to the homomorphic `crypto/ElGamal`: use ElGamal to
13
- * accumulate ciphertexts, use this to hand a recipient the plaintext.
9
+ * Jubjub public key, built on the key agreement in `crypto/Ecdh`. Delivers a
10
+ * value to whoever holds the secret behind a Jubjub public key WITHOUT encoding
11
+ * it in the exponent, so recovery needs no discrete-log search and the value is
12
+ * unbounded within `Uint<128>`. This is the direct-decrypt counterpart to the
13
+ * homomorphic `crypto/ElGamal`: use ElGamal to accumulate ciphertexts, use this
14
+ * to hand a recipient the plaintext.
14
15
  *
15
- * Construction (recipient key `pk = g^ek`, ephemeral scalar `e` fresh per call):
16
- * E = g^e (ephemeral public key)
17
- * S = pk^e = g^{ek*e} (ECDH shared secret point)
18
- * mask = KDF(S) (domain-separated Field, see `kdf`)
19
- * ct = value + mask (field one-time-pad)
20
- * Recipient recovers: S = E^ek, mask = KDF(S), value = ct - mask.
16
+ * @see https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_6.pdf#page=461 Boneh and Shoup, A Graduate Course in Applied Cryptography v0.6, §11.5 ElGamal encryption (E_EG), Remark 11.1 (E'_EG), Theorem 11.4
17
+ * @see https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_6.pdf#page=495 Boneh and Shoup, §12.4 CCA-secure ElGamal encryption, Theorem 12.4
18
+ * @see https://www.shoup.net/papers/twindh.pdf Cash, Kiltz and Shoup, The Twin Diffie-Hellman Problem and Applications, §1.1 Hashed ElGamal Encryption
19
+ * @see https://cseweb.ucsd.edu/~mihir/papers/dhies.pdf Abdalla, Bellare and Rogaway, DHIES, §8 ODH and SDH
20
+ * @see https://www.secg.org/sec1-v2.pdf#page=58 SEC 1 v2.0 §5.1.3, Encryption Operation
21
+ * @see https://www.secg.org/sec1-v2.pdf#page=59 SEC 1 v2.0 §5.1.4, Decryption Operation, action 3
22
+ *
23
+ * @dev Construction and notation. Boneh and Shoup's E'_EG symbol follows in
24
+ * brackets (§11.5, Remark 11.1).
25
+ * - `pk = g^ek`: the recipient's public key [`u = g^α`].
26
+ * - `e`: the ephemeral scalar, fresh per call [`β`].
27
+ * - `E = g^e`: the ephemeral public key [`v = g^β`].
28
+ * - `S = pk^e = g^{ek*e}`: the shared point, from `crypto/Ecdh` [`w = u^β`].
29
+ * - `mask = KDF(S, domain)`: the domain-separated field mask, see `kdf`
30
+ * [`k = H(w)`]. E_EG hashes `(v, w)`; E'_EG hashes `w` alone, as here.
31
+ * - `ct = value + mask`: the field one-time pad [`c = E_s(k, m)`].
32
+ * - `Ciphertext { ephemeralPk: E, ct }`: the ciphertext [`(v, c)`].
33
+ * - Recipient recovers `S = E^ek`, `mask = KDF(S, domain)`, `value = ct - mask`.
34
+ *
35
+ * @dev Key agreement. `deriveShared`, `recoverShared` and the `SharedSecret`
36
+ * struct live in `crypto/Ecdh` and are not re-exported here; a consumer that
37
+ * needs them imports that module directly. It owns the weak-input guards every
38
+ * sender path here inherits.
21
39
  *
22
40
  * @dev Freshness and secrecy precondition (CRITICAL). `e` MUST be both unique per
23
41
  * call to the same recipient AND secret (unpredictable to observers), even across
@@ -25,7 +43,8 @@ pragma language_version >= 0.26.0;
25
43
  *
26
44
  * Uniqueness: the mask is a one-time pad, so a repeated `e` repeats `S` and
27
45
  * `mask`, and reusing a pad is catastrophic (subtracting two ciphertexts recovers
28
- * both values).
46
+ * both values). Padding two values under the same `(S, domain)` reuses the pad
47
+ * the same way (see `kdf`).
29
48
  *
30
49
  * Secrecy: `recipientPk` is public, so anyone who learns `e` recomputes
31
50
  * `S = recipientPk^e`, hence the mask, and opens the ciphertext. A unique but
@@ -38,19 +57,11 @@ pragma language_version >= 0.26.0;
38
57
  * This is stronger than exponential ElGamal's uniqueness requirement (there,
39
58
  * reuse merely leaks plaintext equality); do not treat `e` as "any randomness."
40
59
  *
41
- * @dev Weak inputs. Two degenerate inputs zero the shared secret and expose the
42
- * value: `recipientPk = identity` gives `S = identity` for any `e`, and `e = 0`
43
- * gives `S = identity` for any key. Both yield `mask = KDF(identity, domain)`, a
44
- * value any observer can recompute, so `ct = value + mask` opens trivially. The
45
- * identity is a valid subgroup point, so the runtime subgroup guarantee does NOT
46
- * catch it; `encrypt` therefore asserts `recipientPk != identity` and
47
- * `ephemeralPk (= g^e) != identity`. Guarding the ephemeral POINT rather than the
48
- * scalar subsumes `e != 0` and stays correct regardless of how the runtime treats
49
- * an out-of-range scalar: `g^e = identity` captures both `e = 0` and any
50
- * `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the runtime
51
- * faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that dependency.)
52
- * These are the only weak inputs (for a subgroup key, `S = identity` iff the
53
- * ephemeral point is the identity).
60
+ * @dev Weak inputs. An identity `recipientPk` or a zero `e` collapses `S` to the
61
+ * identity point, giving `mask = KDF(identity, domain)`, a value any observer can
62
+ * recompute, so `ct = value + mask` opens trivially. `crypto/Ecdh.deriveShared`
63
+ * rejects both, and `encrypt` reaches them only through it; that module's
64
+ * weak-inputs note carries the full argument.
54
65
  *
55
66
  * @dev Integrity. The pad is malleable and NO authentication is added here. This
56
67
  * is safe in the intended on-chain use, where the ciphertext is computed
@@ -60,10 +71,13 @@ pragma language_version >= 0.26.0;
60
71
  * enable theft. Consumers with a different threat model (mutable storage,
61
72
  * in-transit tampering) MUST add their own authentication.
62
73
  *
63
- * @dev Security. This is hashed ElGamal (ECIES without a MAC) over Jubjub and
64
- * inherits its guarantees: IND-CPA under the Oracle Diffie-Hellman assumption in
65
- * the random-oracle model (the `persistentHash` KDF keyed by the secret point
66
- * `S`). It is NOT IND-CCA: the additive pad is malleable (see Integrity), which
74
+ * @dev Security. This is hashed ElGamal over Jubjub: Boneh and Shoup, *A
75
+ * Graduate Course in Applied Cryptography* v0.6, §11.5, in the Remark 11.1
76
+ * form that keys the KDF on the shared point alone. It inherits that scheme's
77
+ * guarantee: IND-CPA under CDH in the random-oracle model (Theorem 11.4, the
78
+ * `persistentHash` KDF keyed by the secret point `S`), and so also under the
79
+ * stronger Oracle Diffie-Hellman assumption. It is NOT IND-CCA: Theorem 12.4
80
+ * needs a MAC, and the additive pad is malleable (see Integrity), which
67
81
  * is acceptable here only because the on-chain ciphertext is bound in-circuit to
68
82
  * the value it commits to elsewhere. Hiding carries a large margin: `kdf` returns
69
83
  * a `degradeToTransient` output in `[0, 2^248)` while `value < 2^128`, so even
@@ -72,9 +86,10 @@ pragma language_version >= 0.26.0;
72
86
  * is PRF-like. Confidentiality relies on the runtime constraining every
73
87
  * `JubjubPoint` into the prime-order subgroup (see `crypto/ElGamal`), so `S`
74
88
  * cannot be a low-order point that leaks `ek`. Binding the ephemeral `E` into the
75
- * KDF is deliberately omitted: for a fixed `recipientPk`, `E = g^e` and
76
- * `S = pk^e` are both determined by `e`, so `KDF(S)` already varies with `E` and
77
- * folding it in buys nothing.
89
+ * KDF is deliberately omitted (Remark 11.1): for a fixed `recipientPk`, `E = g^e`
90
+ * and `S = pk^e` are both determined by `e`, so `KDF(S)` already varies with `E`;
91
+ * folding it in only tightens the CCA reduction (Remark 12.2)
92
+ * which this module does not claim.
78
93
  *
79
94
  * @dev Shared key with `crypto/ElGamal` (in scope). A consumer MAY use the same
80
95
  * recipient Jubjub key for both these memos (hashed ElGamal) and exponential-
@@ -82,6 +97,9 @@ pragma language_version >= 0.26.0;
82
97
  * safe: the two schemes share only the public key, this KDF is domain-separated
83
98
  * and keyed by the secret DH point, and neither exposes a decryption oracle, so
84
99
  * under CPA / ODH a ciphertext of one scheme does not help attack the other.
100
+ * The per-encryption scalars MUST be independent (e.g. distinct
101
+ * `expandRandomness` tags over one seed): a shared scalar shares `S`, so the
102
+ * memo becomes a plaintext-check oracle for the ElGamal ciphertext.
85
103
  *
86
104
  * @dev No ledger state, no witnesses. Every circuit is pure and takes its keys,
87
105
  * value, and randomness as explicit arguments. `decrypt` is exposed as a pure
@@ -90,37 +108,22 @@ pragma language_version >= 0.26.0;
90
108
  */
91
109
  module EcdhMask {
92
110
  import CompactStandardLibrary;
111
+ import "./Ecdh" prefix Ecdh_;
93
112
 
94
113
  /**
95
- * @description An ECDH one-time-pad ciphertext: the ephemeral public key and
96
- * the field-masked value.
114
+ * @description An ECDH one-time-pad ciphertext, `(v, c)` in Boneh and Shoup.
115
+ * Public once written; neither field is secret on its own.
116
+ *
117
+ * @notice `ephemeralPk` is never the identity for a ciphertext `encrypt`
118
+ * produced, and `decrypt` rejects one that is. A consumer that hand-builds a
119
+ * placeholder with an identity `ephemeralPk` must document it as a sentinel.
97
120
  */
98
121
  export struct Ciphertext {
122
+ /** `E = g^e`, the sender's ephemeral public key. Never the identity. */
99
123
  ephemeralPk: JubjubPoint;
100
- ct: Field;
101
- }
102
124
 
103
- /**
104
- * @description Derives the field mask from the ECDH shared secret point,
105
- * domain-separated by a caller-supplied `domain` tag. Hashes the point to
106
- * `Bytes<32>`, then re-hashes it with the domain tag and truncates into the
107
- * field via `degradeToTransient`. The consumer chooses `domain` (so this
108
- * module is not tied to any one protocol) and MUST use the same value on
109
- * encrypt and decrypt.
110
- *
111
- * @dev Uses `persistentHash` deliberately: the recipient reproduces this mask
112
- * to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
113
- *
114
- * @param sShared - The ECDH shared secret point (`pk^e` for the sender,
115
- * `E^ek` for the recipient; equal by construction).
116
- * @param domain - The consumer's domain-separation tag.
117
- * @return The field mask.
118
- */
119
- export pure circuit kdf(sShared: JubjubPoint, domain: Bytes<32>): Field {
120
- const pointHash = persistentHash<JubjubPoint>(sShared);
121
- return degradeToTransient(
122
- persistentHash<Vector<2, Bytes<32>>>([pointHash, domain])
123
- );
125
+ /** `value + KDF(S, domain)`, the field one-time pad over the value. */
126
+ ct: Field;
124
127
  }
125
128
 
126
129
  /**
@@ -128,6 +131,8 @@ module EcdhMask {
128
131
  *
129
132
  * @notice `e` MUST be fresh per call (see the module freshness precondition).
130
133
  *
134
+ * @constraints k=14, rows=9269
135
+ *
131
136
  * Requirements:
132
137
  *
133
138
  * - `recipientPk` is not the identity point.
@@ -145,23 +150,47 @@ module EcdhMask {
145
150
  e: JubjubScalar,
146
151
  domain: Bytes<32>
147
152
  ): Ciphertext {
148
- const identity = ecMulGenerator(0 as JubjubScalar);
149
- assert(recipientPk != identity, "EcdhMask: identity pk");
150
- const ephemeralPk = ecMulGenerator(e);
151
- // Guard the ephemeral POINT, not the scalar: `ephemeralPk == identity`
152
- // captures `e == 0` and any `e` that reduces to 0 mod l, so the guard is
153
- // correct regardless of how the runtime treats an out-of-range scalar. This
154
- // subsumes `e != 0` (see the weak-inputs note).
155
- assert(ephemeralPk != identity, "EcdhMask: zero ephemeral");
156
- const sShared = ecMul(recipientPk, e);
157
- const mask = kdf(sShared, domain);
158
- return Ciphertext { ephemeralPk: ephemeralPk, ct: (value as Field) + mask };
153
+ // The weak-input guards come from `deriveShared`, not repeated here.
154
+ const shared = Ecdh_deriveShared(recipientPk, e);
155
+ const mask = kdf(shared.sShared, domain);
156
+ return Ciphertext { ephemeralPk: shared.ephemeralPk, ct: (value as Field) + mask };
157
+ }
158
+
159
+ /**
160
+ * @description Derives the field mask from the ECDH shared secret point and
161
+ * a caller-supplied `domain` tag: hashes the point to `Bytes<32>`, re-hashes
162
+ * it with the tag, and truncates into the field via `degradeToTransient`.
163
+ * The mask is a pure function of `(sShared, domain)`, so the pair MUST match
164
+ * on encrypt and decrypt, and MUST be distinct for every value it pads. Two
165
+ * values padded under one pair share the pad and leak their difference.
166
+ *
167
+ * @dev Uses `persistentHash` deliberately: the recipient reproduces this mask
168
+ * to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
169
+ *
170
+ * @constraints k=13, rows=7917
171
+ *
172
+ * @param sShared - The ECDH shared secret point (`pk^e` for the sender,
173
+ * `E^ek` for the recipient; equal by construction).
174
+ * @param domain - The consumer's domain-separation tag.
175
+ * @return The field mask.
176
+ */
177
+ export pure circuit kdf(sShared: JubjubPoint, domain: Bytes<32>): Field {
178
+ const pointHash = persistentHash<JubjubPoint>(sShared);
179
+ return degradeToTransient(
180
+ persistentHash<Vector<2, Bytes<32>>>([pointHash, domain])
181
+ );
159
182
  }
160
183
 
161
184
  /**
162
185
  * @description Recovers the value from a ciphertext using the recipient's
163
186
  * secret scalar. Pure and off-chain (no proof needed to read a received value).
164
187
  *
188
+ * @notice Rejects a ciphertext whose `ephemeralPk` is the identity point
189
+ * (SEC 1 §5.1.4, action 3). `encrypt` cannot produce one, so a wallet that
190
+ * meets one holds a placeholder, not an encryption.
191
+ *
192
+ * @constraints k=14, rows=8976
193
+ *
165
194
  * @param ciphertext - The ciphertext to decrypt.
166
195
  * @param ekScalar - The recipient's secret scalar (`crypto/ElGamal`'s
167
196
  * `secretToScalar(EK)`).
@@ -170,7 +199,11 @@ module EcdhMask {
170
199
  * which is `< 2^128`).
171
200
  */
172
201
  export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: JubjubScalar, domain: Bytes<32>): Field {
173
- const sShared = ecMul(ciphertext.ephemeralPk, ekScalar);
202
+ // `encrypt` never emits an identity ephemeral; one here is a malformed
203
+ // ciphertext whose mask is a public constant, so refuse rather than decrypt.
204
+ assert(ciphertext.ephemeralPk != ecMulGenerator(0 as JubjubScalar),
205
+ "EcdhMask: identity ephemeral");
206
+ const sShared = Ecdh_recoverShared(ciphertext.ephemeralPk, ekScalar);
174
207
  const mask = kdf(sShared, domain);
175
208
  return ciphertext.ct - mask;
176
209
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/Ecdsa.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/Ecdsa.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.6 (crypto/Eip712.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/Eip712.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.6 (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (crypto/ElGamal.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.6 (multisig/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/ForwarderPrivate.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.6 (multisig/ForwarderShielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/ForwarderShielded.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.6 (multisig/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/ForwarderUnshielded.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.6 (multisig/ProposalManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/ProposalManager.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.6 (multisig/Secp256k1EcdsaMultisig.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/Secp256k1EcdsaMultisig.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.6 (multisig/ShieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/ShieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5