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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +9 -4
  2. package/access/AccessControl.compact +8 -9
  3. package/access/Ownable.compact +21 -20
  4. package/access/ShieldedAccessControl.compact +20 -19
  5. package/access/ZOwnablePK.compact +5 -3
  6. package/crypto/Ecdh.compact +153 -0
  7. package/crypto/EcdhMask.compact +146 -84
  8. package/crypto/Ecdsa.compact +1 -1
  9. package/crypto/Eip712.compact +1 -1
  10. package/crypto/ElGamal.compact +13 -13
  11. package/math/UintN.compact +82 -0
  12. package/multisig/ForwarderPrivate.compact +6 -17
  13. package/multisig/ForwarderShielded.compact +1 -1
  14. package/multisig/ForwarderUnshielded.compact +1 -1
  15. package/multisig/ProposalManager.compact +127 -65
  16. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  17. package/multisig/ShieldedTreasury.compact +8 -20
  18. package/multisig/ShieldedTreasuryStateless.compact +9 -21
  19. package/multisig/Signer.compact +1 -1
  20. package/multisig/UnshieldedTreasury.compact +11 -24
  21. package/multisig/examples/ForwarderPrivateExample.compact +2 -3
  22. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  23. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  24. package/multisig/examples/{ShieldedMultiSigV2Example.compact → NativeShieldedStatelessTreasuryExample.compact} +23 -23
  25. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  26. package/multisig/presets/NativeShieldedStatelessTreasury.compact +443 -0
  27. package/multisig/presets/NativeShieldedTokenIssuer.compact +2 -5
  28. package/package.json +11 -11
  29. package/security/Allowlist.compact +1 -1
  30. package/security/Blocklist.compact +1 -1
  31. package/security/Initializable.compact +3 -1
  32. package/security/Pausable.compact +1 -1
  33. package/token/ConfidentialFungibleToken.compact +22 -70
  34. package/token/ConfidentialNoteFungibleToken.compact +741 -0
  35. package/token/FungibleToken.compact +36 -30
  36. package/token/MultiToken.compact +27 -21
  37. package/token/NativeShieldedToken.compact +4 -12
  38. package/token/NativeShieldedTokenCore.compact +1 -1
  39. package/token/NativeShieldedTokenFamily.compact +4 -4
  40. package/token/NonFungibleToken.compact +28 -22
  41. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  42. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  43. package/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact +274 -0
  44. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  45. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +1 -1
  46. package/token/extensions/NativeShieldedTokenPublicSupply.compact +1 -1
  47. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +1 -1
  48. package/token/presets/ZOwnableNativeShieldedToken.compact +1 -1
  49. package/utils/EvmAbi.compact +1 -1
  50. package/utils/Utils.compact +1 -1
  51. package/multisig/RecipientTypes.compact +0 -48
  52. package/multisig/presets/ShieldedMultiSigV2.compact +0 -295
package/README.md CHANGED
@@ -10,7 +10,7 @@ This project is built on the Midnight Network.
10
10
 
11
11
  > ## ⚠️ WARNING! ⚠️
12
12
  >
13
- > This repo contains highly experimental code.
13
+ > Pre-release versions (`-alpha`, `-rc`) contain experimental, unaudited code.
14
14
  > Expect rapid iteration.
15
15
  > **Use at your own risk.**
16
16
 
@@ -193,7 +193,7 @@ ZKIR v3: the library targets ZKIR v2, the compiler 0.34.0 default.
193
193
  Pass `--feature-zkir-v3` only when the contract reaches a module that needs it:
194
194
  the secp256k1-based `crypto/Ecdsa` and `multisig/Secp256k1EcdsaMultisig`,
195
195
  or the `keccak256`-based `crypto/Eip712`.
196
- `multisig/presets/ShieldedMultiSigV2`, `multisig/presets/NativeShieldedTokenIssuer`
196
+ `multisig/presets/NativeShieldedStatelessTreasury`, `multisig/presets/NativeShieldedTokenIssuer`
197
197
  and the `multisig/examples/` contracts reach both.
198
198
  `utils/EvmAbi` needs no `keccak256` of its own and compiles on either backend.
199
199
  Compiling the other modules under v3 currently hits upstream blockers,
@@ -233,6 +233,8 @@ yarn compile
233
233
  yarn test
234
234
  ```
235
235
 
236
+ Integration and live suites are described in [TESTING.md](TESTING.md).
237
+
236
238
  ### Check/apply Biome formatter
237
239
 
238
240
  ```bash
@@ -247,6 +249,7 @@ yarn lint:fix
247
249
  ```bash
248
250
  yarn compile:access
249
251
  yarn compile:archive
252
+ yarn compile:math
250
253
  ...
251
254
  ```
252
255
 
@@ -278,9 +281,11 @@ rm -rf .turbo/
278
281
 
279
282
  ## Security
280
283
 
281
- This project is still in a very early and experimental phase. It has never been audited nor thoroughly reviewed for security vulnerabilities. DO NOT USE IT IN PRODUCTION.
284
+ For production, use a stable release (no `-alpha` or `-rc` suffix) on the Midnight ledger it targets. Pre-releases are not audited and should not be used in production.
285
+
286
+ Scopes and audited commits are listed in [audits](./audits/README.md).
282
287
 
283
- Please report any security issues you find to <security@openzeppelin.com>.
288
+ Please report any security issues you find to <security@openzeppelin.com>. See [SECURITY.md](SECURITY.md).
284
289
 
285
290
  ### Provenance
286
291
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (access/AccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -61,18 +61,17 @@ pragma language_version >= 0.26.0;
61
61
  * through the main role approval circuits (`grantRole` and `_grantRole`).
62
62
  * In other words, role approvals to contract addresses are disallowed through these
63
63
  * circuits.
64
- * This is because Compact currently does not support contract-to-contract calls which means
65
- * if a contract is granted a role, the contract cannot directly call the protected
66
- * circuit.
64
+ * This is because this library has not yet migrated to contract-to-contract calls, which means
65
+ * a contract granted a role cannot directly call the protected circuit.
67
66
  *
68
67
  * @notice This module does offer an experimental circuit that allows roles to be granted
69
68
  * to contract addresses (`_unsafeGrantRole`).
70
69
  * Note that the circuit name is very explicit ("unsafe") with this experimental circuit.
71
- * Until contract-to-contract calls are supported,
70
+ * Until this library migrates to contract-to-contract calls,
72
71
  * there is no direct way for a contract to call protected circuits.
73
72
  *
74
- * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls
75
- * are supported.
73
+ * @notice The unsafe circuits are planned to become deprecated once this library migrates to
74
+ * contract-to-contract calls.
76
75
  *
77
76
  * @dev Migration plan (safe -> C2C-capable role grants)
78
77
  *
@@ -84,8 +83,8 @@ pragma language_version >= 0.26.0;
84
83
  * which is the only kind of upgrade the CMA supports. Widening the type later would instead force
85
84
  * a ledger-layout rewrite, which the CMA cannot perform.
86
85
  *
87
- * When C2C lands, an already-deployed contract migrates without changing its address or ledger
88
- * state:
86
+ * When the C2C migration lands, an already-deployed contract migrates without changing its address
87
+ * or ledger state:
89
88
  *
90
89
  * - Minor release: mark `_unsafeGrantRole` deprecated (and emit a warning if possible); keep its
91
90
  * implementation intact so existing callers continue to work.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (access/Ownable.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -28,18 +28,17 @@ pragma language_version >= 0.26.0;
28
28
  * through the main transfer circuits (`transferOwnership` and `_transferOwnership`).
29
29
  * In other words, ownership transfers to contract addresses are disallowed through these
30
30
  * circuits.
31
- * This is because Compact currently does not support contract-to-contract calls which means
32
- * if a contract is granted ownership, the owner contract cannot directly call the protected
33
- * circuit.
31
+ * This is because this library has not yet migrated to contract-to-contract calls, which means
32
+ * a contract granted ownership cannot directly call the protected circuit.
34
33
  *
35
34
  * @notice This module does offer experimental circuits that allow ownership to be granted
36
35
  * to contract addresses (`_unsafeTransferOwnership` and `_unsafeUncheckedTransferOwnership`).
37
36
  * Note that the circuit names are very explicit ("unsafe") with these experimental circuits.
38
- * Until contract-to-contract calls are supported, there is no direct way for a contract
37
+ * Until this library migrates to contract-to-contract calls, there is no direct way for a contract
39
38
  * to authenticate as an owner.
40
39
  *
41
- * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls
42
- * are supported.
40
+ * @notice The unsafe circuits are planned to become deprecated once this library migrates to
41
+ * contract-to-contract calls.
43
42
  *
44
43
  * @dev Migration plan (safe -> C2C-capable ownership transfers)
45
44
  *
@@ -51,8 +50,8 @@ pragma language_version >= 0.26.0;
51
50
  * which is the only kind of upgrade the CMA supports. Widening the type later would instead force
52
51
  * a ledger-layout rewrite, which the CMA cannot perform.
53
52
  *
54
- * When C2C lands, an already-deployed contract migrates without changing its address or ledger
55
- * state:
53
+ * When the C2C migration lands, an already-deployed contract migrates without changing its address
54
+ * or ledger state:
56
55
  *
57
56
  * - Minor release: mark `_unsafeTransferOwnership` (and `_unsafeUncheckedTransferOwnership`)
58
57
  * deprecated (and emit a warning if possible); keep their implementation intact so existing
@@ -117,6 +116,8 @@ module Ownable {
117
116
  * @description Initializes the contract by setting the `initialOwner`.
118
117
  * This must be called in the contract's constructor.
119
118
  *
119
+ * @constraints k=10, rows=626
120
+ *
120
121
  * Requirements:
121
122
  *
122
123
  * - Contract is not already initialized.
@@ -178,8 +179,8 @@ module Ownable {
178
179
  /**
179
180
  * @description Transfers ownership of the contract to `newOwner`.
180
181
  *
181
- * @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
182
- * interactions are supported in Compact.
182
+ * @notice Ownership transfers to contract addresses are disallowed until this library migrates to
183
+ * contract-to-contract calls.
183
184
  * This restriction prevents permanently disabling access to a circuit.
184
185
  *
185
186
  * @constraints k=13, rows=2947
@@ -204,10 +205,10 @@ module Ownable {
204
205
  /**
205
206
  * @description Unsafe variant of `transferOwnership`.
206
207
  *
207
- * @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
208
- * calls are not currently supported.
208
+ * @warning Ownership transfers to contract addresses are considered unsafe because this library
209
+ * has not yet migrated to contract-to-contract calls.
209
210
  * Ownership privileges sent to a contract address may become uncallable.
210
- * Once contract-to-contract calls are supported, this circuit may be deprecated.
211
+ * Once this library migrates to contract-to-contract calls, this circuit may be deprecated.
211
212
  *
212
213
  * @constraints k=13, rows=2944
213
214
  *
@@ -246,7 +247,7 @@ module Ownable {
246
247
  assertOnlyOwner();
247
248
 
248
249
  // We use `left` here because `assertOnlyOwner` would return a misleading
249
- // error msg (c2c is not currently supported)
250
+ // error msg (this library has not yet migrated to c2c)
250
251
  const zero = Either<Bytes<32>, ContractAddress> {
251
252
  is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
252
253
  };
@@ -282,8 +283,8 @@ module Ownable {
282
283
  * @description Transfers ownership of the contract to `newOwner` without
283
284
  * enforcing permission checks on the caller.
284
285
  *
285
- * @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
286
- * interactions are supported in Compact.
286
+ * @notice Ownership transfers to contract addresses are disallowed until this library migrates to
287
+ * contract-to-contract calls.
287
288
  * This restriction prevents circuits from being inadvertently locked in contracts.
288
289
  *
289
290
  * @constraints k=10, rows=598
@@ -306,10 +307,10 @@ module Ownable {
306
307
  /**
307
308
  * @description Unsafe variant of `_transferOwnership`.
308
309
  *
309
- * @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
310
- * calls are not currently supported.
310
+ * @warning Ownership transfers to contract addresses are considered unsafe because this library
311
+ * has not yet migrated to contract-to-contract calls.
311
312
  * Ownership privileges sent to a contract address may become uncallable.
312
- * Once contract-to-contract calls are supported, this circuit may be deprecated.
313
+ * Once this library migrates to contract-to-contract calls, this circuit may be deprecated.
313
314
  *
314
315
  * @constraints k=10, rows=595
315
316
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (access/ShieldedAccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -140,8 +140,14 @@ pragma language_version >= 0.26.0;
140
140
  * between roles should use separate keys per role and accept the additional key management overhead.
141
141
  * In the event of key compromise, the user should coordinate with admins to revoke all roles
142
142
  * under the compromised `accountId` and re-grant under a new identity.
143
- * - A `(role, accountId)` pairing can only be revoked after it was granted, so the nullifier
144
- * set holds at most one entry per granted pairing.
143
+ * - Admins can revoke `(role, accountId)` pairings that were never granted, since on-chain
144
+ * non-membership proofs are not available for the Merkle tree. This permanently burns the
145
+ * nullifier for that pairing, blocking any future grant. Admin trust is a fundamental
146
+ * assumption of this system. Off-chain validation should confirm that a pairing was actually
147
+ * granted before submitting a revocation transaction.
148
+ * - `renounceRole` allows burning nullifiers for roles the caller does not currently hold,
149
+ * provided they possess the correct `secretKey`. This requires knowledge of the secret key,
150
+ * at which point the `accountId` should be considered fully compromised regardless.
145
151
  * - The Merkle tree capacity of 2^20 leaf slots is a permanent, irrecoverable resource.
146
152
  * There is no mechanism to reclaim slots after revocation, no upgrade path, and no emergency
147
153
  * recovery. Once exhausted, the contract is permanently unable to grant new roles. Deployers
@@ -206,8 +212,7 @@ module ShieldedAccessControl {
206
212
 
207
213
  /**
208
214
  * @description Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so
209
- * one pairing can never consume more than one `_operatorRoles` slot, and gates revocation
210
- * so a nullifier can only exist for a granted pairing.
215
+ * one pairing can never consume more than one `_operatorRoles` slot.
211
216
  * @type {Set<RoleNullifier>} _grantedRoleNullifiers
212
217
  */
213
218
  export ledger _grantedRoleNullifiers: Set<RoleNullifier>;
@@ -469,17 +474,15 @@ module ShieldedAccessControl {
469
474
  * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's
470
475
  * purpose is to provide a mechanism for accounts to lose their privileges
471
476
  * if they are compromised (such as when a trusted device is misplaced).
472
- * Consumers with no need for self-revocation should not export this circuit.
473
477
  *
474
478
  * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity
475
479
  * guarantees if renounceRole is used in tandem with other on-chain actions.
476
480
  *
477
- * @constraints k=14, rows=14442
481
+ * @constraints k=14, rows=14440
478
482
  *
479
483
  * Requirements:
480
484
  *
481
485
  * - The caller must provide a valid `accountId` for the role.
482
- * - The `(role, accountId)` pairing must have been granted.
483
486
  * - The `(role, accountId)` pairing must not be already revoked.
484
487
  * - Contract is initialized.
485
488
  *
@@ -505,15 +508,15 @@ module ShieldedAccessControl {
505
508
  /**
506
509
  * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
507
510
  * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
508
- * `role`.
511
+ * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
512
+ * so a `(role, accountId)` pairing that does not exist can still be revoked.
509
513
  *
510
- * @constraints k=15, rows=27007
514
+ * @constraints k=15, rows=27005
511
515
  *
512
516
  * Requirements:
513
517
  *
514
518
  * - caller must prove they're an admin for `role`.
515
519
  * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
516
- * - The `(role, accountId)` pairing must have been granted.
517
520
  * - The `(role, accountId)` pairing must not be already revoked.
518
521
  * - Contract is initialized.
519
522
  *
@@ -538,18 +541,18 @@ module ShieldedAccessControl {
538
541
  /**
539
542
  * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
540
543
  * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
541
- * `role`.
544
+ * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
545
+ * so a `(role, accountId)` pairing that does not exist can still be revoked.
542
546
  *
543
547
  * Internal circuit without access restriction.
544
548
  *
545
549
  * @warning Exposing this circuit directly in an implementing contract would allow anyone to revoke
546
550
  * roles without authorization. It must be wrapped with appropriate access control.
547
551
  *
548
- * @constraints k=14, rows=10262
552
+ * @constraints k=14, rows=10260
549
553
  *
550
554
  * Requirements:
551
555
  *
552
- * - The `(role, accountId)` pairing must have been granted.
553
556
  * - The `(role, accountId)` pairing must not be already revoked.
554
557
  * - Contract is initialized.
555
558
  *
@@ -571,8 +574,8 @@ module ShieldedAccessControl {
571
574
  * @description Core business logic for the grant/revoke role circuits. Asserts that the
572
575
  * `(role, accountId)` pairing has not already been revoked. On success, dispatches on
573
576
  * `updateType`: a `Grant` inserts the role commitment into `_operatorRoles` unless the
574
- * nullifier is already in `_grantedRoleNullifiers`, and a `Revoke` asserts the nullifier
575
- * is in `_grantedRoleNullifiers` before inserting it into `_roleCommitmentNullifiers`.
577
+ * nullifier is already in `_grantedRoleNullifiers`, and a `Revoke` inserts the nullifier
578
+ * into `_roleCommitmentNullifiers`.
576
579
  *
577
580
  * Disclosures:
578
581
  *
@@ -600,14 +603,12 @@ module ShieldedAccessControl {
600
603
  const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier));
601
604
  assert(!isRevoked, "ShieldedAccessControl: role is already revoked");
602
605
 
603
- const isGranted = _grantedRoleNullifiers.member(disclose(roleNullifier));
604
606
  if (updateType == UpdateType.Grant) {
605
- if (!isGranted) {
607
+ if (!_grantedRoleNullifiers.member(disclose(roleNullifier))) {
606
608
  _grantedRoleNullifiers.insert(disclose(roleNullifier));
607
609
  _operatorRoles.insert(disclose(roleCommitment));
608
610
  }
609
611
  } else {
610
- assert(isGranted, "ShieldedAccessControl: role was never granted");
611
612
  _roleCommitmentNullifiers.insert(disclose(roleNullifier));
612
613
  }
613
614
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (access/ZOwnablePK.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -15,8 +15,8 @@ pragma language_version >= 0.26.0;
15
15
  * For the strongest security guarantees, use an Air-Gapped Public Key.
16
16
  *
17
17
  * @notice This module explicitly supports commitments derived from public keys;
18
- * however, it may be possible to use contract addresses when contract-to-contract
19
- * calls become available. This will be revisited when it's known if/how witnesses
18
+ * however, it may be possible to use contract addresses once this library migrates to
19
+ * contract-to-contract calls. This will be revisited when it's known if/how witnesses
20
20
  * are used from a contract address context.
21
21
  *
22
22
  * @dev Features:
@@ -106,6 +106,8 @@ module ZOwnablePK {
106
106
  * @warning The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm.
107
107
  * Using any other algorithm will result in a permanent loss of contract access.
108
108
  *
109
+ * @constraints k=13, rows=6765
110
+ *
109
111
  * Requirements:
110
112
  *
111
113
  * - Contract is not initialized.
@@ -0,0 +1,153 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (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
+ }