@openzeppelin/compact-contracts 0.3.0-alpha.1 → 0.4.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 (42) hide show
  1. package/README.md +10 -4
  2. package/access/AccessControl.compact +2 -2
  3. package/access/Ownable.compact +2 -2
  4. package/access/ShieldedAccessControl.compact +2 -2
  5. package/access/ZOwnablePK.compact +2 -2
  6. package/crypto/EcdhMask.compact +5 -5
  7. package/crypto/Ecdsa.compact +111 -0
  8. package/crypto/ElGamal.compact +33 -66
  9. package/multisig/EcdsaSignerManager.compact +229 -0
  10. package/multisig/ForwarderPrivate.compact +3 -3
  11. package/multisig/ForwarderShielded.compact +3 -3
  12. package/multisig/ForwarderUnshielded.compact +3 -3
  13. package/multisig/ProposalManager.compact +36 -2
  14. package/multisig/ShieldedTreasury.compact +35 -5
  15. package/multisig/ShieldedTreasuryStateless.compact +47 -18
  16. package/multisig/Signer.compact +46 -42
  17. package/multisig/UnshieldedTreasury.compact +45 -18
  18. package/multisig/presets/ShieldedMultiSig.compact +5 -5
  19. package/multisig/presets/ShieldedMultiSigV2.compact +72 -127
  20. package/multisig/presets/ShieldedMultiSigV3.compact +83 -140
  21. package/multisig/presets/forwarder/ForwarderPrivate.compact +2 -2
  22. package/multisig/presets/forwarder/ForwarderShielded.compact +2 -2
  23. package/multisig/presets/forwarder/ForwarderUnshielded.compact +2 -2
  24. package/package.json +22 -20
  25. package/security/Allowlist.compact +2 -2
  26. package/security/Blocklist.compact +2 -2
  27. package/security/Initializable.compact +2 -2
  28. package/security/Pausable.compact +2 -2
  29. package/token/ConfidentialFungibleToken.compact +46 -18
  30. package/token/FungibleToken.compact +2 -2
  31. package/token/MultiToken.compact +2 -2
  32. package/token/NativeShieldedToken.compact +3 -3
  33. package/token/NativeShieldedTokenCore.compact +2 -2
  34. package/token/NativeShieldedTokenFamily.compact +3 -3
  35. package/token/NonFungibleToken.compact +2 -2
  36. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +2 -2
  37. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
  38. package/token/extensions/{NativeShieldedTokenFamilySupply.compact → NativeShieldedTokenFamilyPublicSupply.compact} +10 -10
  39. package/token/extensions/{NativeShieldedTokenSupply.compact → NativeShieldedTokenPublicSupply.compact} +10 -10
  40. package/token/extensions/{NativeShieldedTokenSupplyCore.compact → NativeShieldedTokenPublicSupplyCore.compact} +10 -10
  41. package/utils/Utils.compact +2 -2
  42. package/multisig/SignerManager.compact +0 -205
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.31.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/minokawa-0-18-26-0)
1
+ [![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.34.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/)
2
2
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
3
3
 
4
4
  This project is built on the Midnight Network.
@@ -37,8 +37,8 @@ Follow Midnight's [Compact Developer Tools installation guide](https://docs.midn
37
37
  ```bash
38
38
  $ compact compile --version
39
39
 
40
- Compactc version: 0.31.0
41
- 0.31.0
40
+ Compactc version: 0.34.0
41
+ 0.34.0
42
42
  ```
43
43
 
44
44
  ### Installation
@@ -64,7 +64,7 @@ Import the modules through `./node_modules/@openzeppelin/compact-contracts/...`.
64
64
  ```typescript
65
65
  // MyContract.compact
66
66
 
67
- pragma language_version >= 0.21.0;
67
+ pragma language_version >= 0.26.0;
68
68
 
69
69
  import CompactStandardLibrary;
70
70
  import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"
@@ -189,6 +189,12 @@ Compiling 14 circuits:
189
189
  Overall progress [====================] 14/14
190
190
  ```
191
191
 
192
+ ZKIR v3: the library targets ZKIR v2, the compiler 0.34.0 default. Pass
193
+ `--feature-zkir-v3` only when the contract imports the secp256k1-based modules
194
+ (`crypto/Ecdsa`, `multisig/EcdsaSignerManager`, `ShieldedMultiSigV2`,
195
+ `ShieldedMultiSigV3`). Compiling the other modules under v3 currently hits
196
+ upstream blockers, listed under [Known issues](CHANGELOG.md#known-issues).
197
+
192
198
  ## Development
193
199
 
194
200
  OpenZeppelin Contracts for Compact exists thanks to its contributors.
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/AccessControl.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module AccessControl
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/Ownable.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module Ownable
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/ShieldedAccessControl.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module ShieldedAccessControl
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/ZOwnablePK.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module ZOwnablePK
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (crypto/EcdhMask.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (crypto/EcdhMask.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module EcdhMask
@@ -142,10 +142,10 @@ module EcdhMask {
142
142
  export pure circuit encrypt(
143
143
  recipientPk: JubjubPoint,
144
144
  value: Uint<128>,
145
- e: Field,
145
+ e: JubjubScalar,
146
146
  domain: Bytes<32>
147
147
  ): Ciphertext {
148
- const identity = ecMulGenerator(0 as Field);
148
+ const identity = ecMulGenerator(0 as JubjubScalar);
149
149
  assert(recipientPk != identity, "EcdhMask: identity pk");
150
150
  const ephemeralPk = ecMulGenerator(e);
151
151
  // Guard the ephemeral POINT, not the scalar: `ephemeralPk == identity`
@@ -169,7 +169,7 @@ module EcdhMask {
169
169
  * @return The recovered value as a `Field` (equal to the original `value`,
170
170
  * which is `< 2^128`).
171
171
  */
172
- export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: Field, domain: Bytes<32>): Field {
172
+ export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: JubjubScalar, domain: Bytes<32>): Field {
173
173
  const sShared = ecMul(ciphertext.ephemeralPk, ekScalar);
174
174
  const mask = kdf(sShared, domain);
175
175
  return ciphertext.ct - mask;
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (crypto/Ecdsa.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module Ecdsa
8
+ * @description The low-s rule for secp256k1 ECDSA signatures.
9
+ *
10
+ * `secp256k1EcdsaVerify` accepts both `(r, s)` and `(r, n - s)`, so every valid
11
+ * signature has a second valid encoding that anyone can derive without the
12
+ * signing key. Its own documentation leaves the low-s constraint to the caller.
13
+ * This module is that constraint: reject `s > n / 2` and each signature has one
14
+ * encoding, which is what replay sets, nullifiers, and signature-derived
15
+ * identifiers rely on.
16
+ */
17
+ module Ecdsa {
18
+ import CompactStandardLibrary;
19
+
20
+ /**
21
+ * @description Returns whether `s` is canonical, i.e. `s <= n / 2` where `n`
22
+ * is the secp256k1 group order.
23
+ *
24
+ * @circuitInfo k=13, rows=5125
25
+ *
26
+ * @param {Secp256k1Scalar} s - The `s` component of a signature.
27
+ * @returns {Boolean} True if `s` is in the low half.
28
+ */
29
+ export pure circuit isLowS(s: Secp256k1Scalar): Boolean {
30
+ const sBytes = (s as Bytes<32>) as Vector<32, Uint<8>>;
31
+ return fold(compareByteLe, true, sBytes, SECP256K1_HALF_ORDER_LE());
32
+ }
33
+
34
+ /**
35
+ * @description Fold callback over one little-endian byte pair. A differing
36
+ * pair overrides the running result, so the accumulator ends up holding the
37
+ * comparison at the most significant byte where `s` and `n / 2` differ. All
38
+ * pairs equal leaves the seed untouched, which is what makes the comparison
39
+ * inclusive.
40
+ *
41
+ * @param {Boolean} isLow - Result from the less significant bytes.
42
+ * @param {Uint<8>} sByte - Byte of `s`.
43
+ * @param {Uint<8>} halfByte - Byte of `n / 2`.
44
+ * @returns {Boolean} Updated result.
45
+ */
46
+ pure circuit compareByteLe(
47
+ isLow: Boolean,
48
+ sByte: Uint<8>,
49
+ halfByte: Uint<8>
50
+ ): Boolean {
51
+ return sByte == halfByte ? isLow : sByte < halfByte;
52
+ }
53
+
54
+ /**
55
+ * @description `n / 2` as little-endian bytes, matching the byte order of
56
+ * `Secp256k1Scalar as Bytes<32>`.
57
+ *
58
+ * @returns {Vector<32, Uint<8>>} The bytes of `n / 2`, least significant first.
59
+ */
60
+ pure circuit SECP256K1_HALF_ORDER_LE(): Vector<32, Uint<8>> {
61
+ // 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0
62
+ return [
63
+ 160, 32, 27, 104, 70, 47, 233, 223,
64
+ 29, 80, 164, 87, 115, 110, 87, 93,
65
+ 255, 255, 255, 255, 255, 255, 255, 255,
66
+ 255, 255, 255, 255, 255, 255, 255, 127
67
+ ];
68
+ }
69
+
70
+ // ─── Verification ───────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * @description Rejects a signature in the high half.
74
+ *
75
+ * @circuitInfo k=13, rows=5156
76
+ *
77
+ * Requirements:
78
+ *
79
+ * - `sig.s` must satisfy `s <= n / 2`.
80
+ *
81
+ * @param {Secp256k1EcdsaSignature} sig - The signature to check.
82
+ * @returns {[]} Empty tuple.
83
+ */
84
+ export pure circuit assertLowS(sig: Secp256k1EcdsaSignature): [] {
85
+ assert(isLowS(sig.s), "Ecdsa: high-s signature");
86
+ }
87
+
88
+ /**
89
+ * @description Verifies `sig` over `msgHash` under `pk`, accepting only the
90
+ * low-s encoding. Reports the result instead of asserting, so a caller can
91
+ * fold it into a count or a threshold.
92
+ *
93
+ * @circuitInfo k=16, rows=34056
94
+ *
95
+ * @param {Bytes<32>} msgHash - The digest that was signed.
96
+ * @param {Secp256k1EcdsaSignature} sig - The signature over `msgHash`.
97
+ * @param {Secp256k1Point} pk - The signer's public key.
98
+ * @returns {Boolean} True if the signature is valid and canonical.
99
+ */
100
+ export pure circuit secp256k1EcdsaVerifyLowS(
101
+ msgHash: Bytes<32>,
102
+ sig: Secp256k1EcdsaSignature,
103
+ pk: Secp256k1Point
104
+ ): Boolean {
105
+ // TODO: inline into the return once a guarded stdlib verify compiles.
106
+ // Short-circuiting `&&` puts it behind a guard, which trips an internal
107
+ // error in the compiler's missing-guard-workarounds pass.
108
+ const verified = secp256k1EcdsaVerify(msgHash, sig, pk);
109
+ return isLowS(sig.s) && verified;
110
+ }
111
+ }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (crypto/ElGamal.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module ElGamal
@@ -32,7 +32,11 @@ pragma language_version >= 0.23.0;
32
32
  * and compared against on every later verification; `degradeToTransient`
33
33
  * truncates the Field to 31 bytes so the result is ALWAYS a valid Jubjub
34
34
  * scalar. Feeding a raw `persistentHash` output into `ecMulGenerator` would
35
- * occasionally exceed the Jubjub scalar field order and fault at runtime.
35
+ * occasionally exceed the Jubjub scalar field order and be rejected.
36
+ *
37
+ * @dev Scalars are typed `JubjubScalar`, not `Field`. A `Field as JubjubScalar`
38
+ * cast reduces modulo the subgroup order, so casting at a call site would
39
+ * silently accept an out-of-range scalar the typed parameter rejects.
36
40
  *
37
41
  * @dev TRUST ASSUMPTION — subgroup membership. This is the module's most
38
42
  * load-bearing assumption. Every `JubjubPoint` reaching a curve operation is
@@ -43,9 +47,8 @@ pragma language_version >= 0.23.0;
43
47
  * off-curve / low-order / mixed-order point makes the circuit unsatisfiable.
44
48
  * The module therefore performs NO in-circuit subgroup check: the runtime
45
49
  * 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.
50
+ * `ℓ*P == O` is unavailable anyway because `ecMul` by `ℓ` is out of scalar
51
+ * range. The homomorphic algebra depends on this holding.
49
52
  *
50
53
  * Basis (and its limits): source-verified against `midnight-circuits` 6.0.0 —
51
54
  * the version vendored by the Midnight onchain-runtime we compile against —
@@ -103,10 +106,10 @@ module ElGamal {
103
106
  * module-level hash-to-scalar note for why `degradeToTransient` is required.
104
107
  *
105
108
  * @param secret - The 32-byte secret to map.
106
- * @return A Field guaranteed to be a valid Jubjub scalar.
109
+ * @return A valid Jubjub scalar.
107
110
  */
108
- export pure circuit secretToScalar(secret: Bytes<32>): Field {
109
- return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret]));
111
+ export pure circuit secretToScalar(secret: Bytes<32>): JubjubScalar {
112
+ return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret])) as JubjubScalar;
110
113
  }
111
114
 
112
115
  /**
@@ -129,49 +132,16 @@ module ElGamal {
129
132
  *
130
133
  * @param seed - The seed to expand.
131
134
  * @param tag - A 32-byte tag distinguishing this expansion from its siblings.
132
- * @return A Field suitable for use as encryption randomness.
135
+ * @return A scalar suitable for use as encryption randomness.
133
136
  */
134
- export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>): Field {
135
- return degradeToTransient(persistentHash<Vector<2, Bytes<32>>>([seed, tag]));
137
+ export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>): JubjubScalar {
138
+ return degradeToTransient(persistentHash<Vector<2, Bytes<32>>>([seed, tag])) as JubjubScalar;
136
139
  }
137
140
 
138
141
  // ---------------------------------------------------------------------------
139
142
  // Curve helpers
140
143
  // ---------------------------------------------------------------------------
141
144
 
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
145
  // ---------------------------------------------------------------------------
176
146
  // Encryption & homomorphic operations
177
147
  // ---------------------------------------------------------------------------
@@ -186,7 +156,7 @@ module ElGamal {
186
156
  * before treating it as private.
187
157
  */
188
158
  export pure circuit encryptZero(): Ciphertext {
189
- const id = ecMulGenerator(0 as Field);
159
+ const id = ecMulGenerator(0 as JubjubScalar);
190
160
  return Ciphertext { c1: id, c2: id };
191
161
  }
192
162
 
@@ -214,9 +184,9 @@ module ElGamal {
214
184
  * @param r - The encryption randomness.
215
185
  * @return The ciphertext `(g^r, pk^r * m)`.
216
186
  */
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");
187
+ export pure circuit encryptPoint(pk: JubjubPoint, m: JubjubPoint, r: JubjubScalar): Ciphertext {
188
+ assert(pk != ecMulGenerator(0 as JubjubScalar), "ElGamal: identity pk");
189
+ assert(r != 0 as JubjubScalar, "ElGamal: zero randomness");
220
190
  const c1 = ecMulGenerator(r);
221
191
  const mask = ecMul(pk, r);
222
192
  const c2 = ecAdd(mask, m);
@@ -226,8 +196,8 @@ module ElGamal {
226
196
  /**
227
197
  * @description Produces a fresh ciphertext of `value` under `pk` with
228
198
  * 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.
199
+ * as a scalar directly to avoid a redundant conversion at every call site;
200
+ * `expandRandomness` produces one for the same reason.
231
201
  *
232
202
  * Requirements:
233
203
  *
@@ -239,8 +209,8 @@ module ElGamal {
239
209
  * @param r - The encryption randomness.
240
210
  * @return The ciphertext `(g^r, pk^r * g^value)`.
241
211
  */
242
- export pure circuit encrypt(pk: JubjubPoint, value: Uint<128>, r: Field): Ciphertext {
243
- return encryptPoint(pk, ecMulGenerator(value as Field), r);
212
+ export pure circuit encrypt(pk: JubjubPoint, value: Uint<128>, r: JubjubScalar): Ciphertext {
213
+ return encryptPoint(pk, ecMulGenerator(value as JubjubScalar), r);
244
214
  }
245
215
 
246
216
  /**
@@ -310,7 +280,7 @@ module ElGamal {
310
280
  * @param k - The public scalar multiplier (a valid Jubjub scalar).
311
281
  * @return A ciphertext of `k * v`.
312
282
  */
313
- export pure circuit scalarMul(ct: Ciphertext, k: Field): Ciphertext {
283
+ export pure circuit scalarMul(ct: Ciphertext, k: JubjubScalar): Ciphertext {
314
284
  return Ciphertext { c1: ecMul(ct.c1, k), c2: ecMul(ct.c2, k) };
315
285
  }
316
286
 
@@ -329,7 +299,7 @@ module ElGamal {
329
299
  old: Ciphertext,
330
300
  pk: JubjubPoint,
331
301
  value: Uint<128>,
332
- r: Field
302
+ r: JubjubScalar
333
303
  ): Ciphertext {
334
304
  return add(old, encrypt(pk, value, r));
335
305
  }
@@ -352,7 +322,7 @@ module ElGamal {
352
322
  old: Ciphertext,
353
323
  pk: JubjubPoint,
354
324
  value: Uint<128>,
355
- r: Field
325
+ r: JubjubScalar
356
326
  ): Ciphertext {
357
327
  return sub(old, encrypt(pk, value, r));
358
328
  }
@@ -380,7 +350,7 @@ module ElGamal {
380
350
  * @param r - Fresh, nonzero randomness for the rerandomization.
381
351
  * @return A fresh ciphertext of the same plaintext.
382
352
  */
383
- export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: Field): Ciphertext {
353
+ export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: JubjubScalar): Ciphertext {
384
354
  return add(ct, encrypt(pk, 0, r));
385
355
  }
386
356
 
@@ -428,17 +398,14 @@ module ElGamal {
428
398
  m: JubjubPoint
429
399
  ): [] {
430
400
  // Inlines the same key check as `assertKeyPair` (rather than calling it) so
431
- // `ekField` is computed once and reused for the decryption below, avoiding
401
+ // `ekScalar` is computed once and reused for the decryption below, avoiding
432
402
  // a second `secretToScalar` hash in this hot verification path.
433
- const ekField = secretToScalar(ek);
434
- assert(ecMulGenerator(ekField) == pk,
403
+ const ekScalar = secretToScalar(ek);
404
+ assert(ecMulGenerator(ekScalar) == pk,
435
405
  "ElGamal: ek/pk mismatch");
436
406
 
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);
407
+ // m = c2 - ek*c1, with ek*c1 negated via ecNeg.
408
+ const ekC1 = ecMul(ct.c1, ekScalar);
442
409
  const mPoint = ecAdd(ct.c2, ecNeg(ekC1));
443
410
  assert(mPoint == m,
444
411
  "ElGamal: plaintext mismatch");
@@ -468,6 +435,6 @@ module ElGamal {
468
435
  ek: Bytes<32>,
469
436
  claimedValue: Uint<128>
470
437
  ): [] {
471
- assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as Field));
438
+ assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as JubjubScalar));
472
439
  }
473
440
  }
@@ -0,0 +1,229 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/EcdsaSignerManager.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module EcdsaSignerManager
8
+ * @description Threshold authorization over secp256k1 ECDSA approvals for
9
+ * multisig contracts.
10
+ *
11
+ * Signer identities are stored as commitments: `persistentHash` over the
12
+ * signer's public-key coordinates, an instance salt, and a domain separator.
13
+ * The salt is per-deployment, so the same public key yields a different
14
+ * commitment in every instance and cannot be correlated across contracts.
15
+ *
16
+ * Wraps `Signer<Bytes<32>>` for the registry and threshold. Consuming
17
+ * contracts own the digest: they compute the message hash their signers
18
+ * signed, then hand it to `assertApprovals` with the parallel public-key and
19
+ * signature vectors.
20
+ *
21
+ * The module's ledger is not exported, so a contract importing it under a
22
+ * prefix keeps the salt and the signer registry out of its generated
23
+ * `ledger()` reader.
24
+ */
25
+ module EcdsaSignerManager {
26
+ import CompactStandardLibrary;
27
+ import "../crypto/Ecdsa" prefix Ecdsa_;
28
+ import "./Signer"<Bytes<32>> prefix Signer_;
29
+
30
+ // ─── State ──────────────────────────────────────────────────────────────────
31
+
32
+ ledger _instanceSalt: Bytes<32>;
33
+
34
+ // ─── Types ──────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * @description Input to `persistentHash` for computing signer commitments.
38
+ */
39
+ struct SignerCommitmentInput {
40
+ pkX: Bytes<32>,
41
+ pkY: Bytes<32>,
42
+ salt: Bytes<32>,
43
+ domain: Bytes<32>
44
+ }
45
+
46
+ /**
47
+ * @description Accumulator threaded through the approval fold: the running
48
+ * valid count, the previous commitment (for duplicate detection), and the
49
+ * digest every signature is checked against.
50
+ */
51
+ struct VerificationState {
52
+ validCount: Uint<8>,
53
+ prevCommitment: Bytes<32>,
54
+ msgHash: Bytes<32>
55
+ }
56
+
57
+ // ─── Initialization ─────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * @description Stores the instance salt and registers the initial signer set.
61
+ * Call from the consuming contract's constructor only.
62
+ *
63
+ * Requirements:
64
+ *
65
+ * - Can only be called once.
66
+ * - `instanceSalt` should be cryptographically random.
67
+ * - `signerCommitments` must not contain duplicates.
68
+ * - `threshold` must be non-zero and must not exceed `n`.
69
+ * - `threshold` must not exceed 2, the number of approvals `assertApprovals`
70
+ * verifies.
71
+ *
72
+ * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
73
+ * @param {Vector<n, Bytes<32>>} signerCommitments - Hashed signer identities.
74
+ * @param {Uint<8>} threshold - Minimum approvals required.
75
+ * @returns {[]} Empty tuple.
76
+ */
77
+ export circuit initialize<#n>(
78
+ instanceSalt: Bytes<32>,
79
+ signerCommitments: Vector<n, Bytes<32>>,
80
+ threshold: Uint<8>
81
+ ): [] {
82
+ _instanceSalt = disclose(instanceSalt);
83
+ assert(threshold <= 2, "EcdsaSignerManager: threshold cannot exceed 2 (assertApprovals verifies 2 signatures)");
84
+ Signer_initialize<n>(signerCommitments, threshold);
85
+ }
86
+
87
+ // ─── Commitments ────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * @description Computes a signer commitment from a secp256k1 public key,
91
+ * as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
92
+ *
93
+ * Pure, so the deployer can derive constructor commitments off-chain the
94
+ * same way the circuits derive them in-circuit.
95
+ *
96
+ * Requirements:
97
+ *
98
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
99
+ *
100
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
101
+ * @param {Bytes<32>} salt - The instance salt.
102
+ * @returns {Bytes<32>} The signer commitment.
103
+ */
104
+ export pure circuit calculateSignerId(
105
+ pk: Secp256k1Point,
106
+ salt: Bytes<32>
107
+ ): Bytes<32> {
108
+ return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
109
+ pkX: secp256k1PointX(pk) as Bytes<32>,
110
+ pkY: secp256k1PointY(pk) as Bytes<32>,
111
+ salt: salt,
112
+ domain: pad(32, "multisig:signer:")
113
+ });
114
+ }
115
+
116
+ // ─── Approvals ──────────────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * @description Verifies two approvals over `msgHash` and asserts the
120
+ * threshold is met.
121
+ *
122
+ * `pubkeys` and `signatures` are parallel: entry `i` of each belongs to the
123
+ * same signer.
124
+ *
125
+ * @dev Both entries are asserted, so the count handed to the threshold check
126
+ * is always 2 and a threshold below 2 admits nothing extra.
127
+ *
128
+ * @circuitInfo k=17, rows=64916
129
+ *
130
+ * Requirements:
131
+ *
132
+ * - Every public key must hash to a registered signer commitment.
133
+ * - Every signature must be valid over `msgHash` and use the low-s encoding.
134
+ * - Signers must not be duplicates.
135
+ *
136
+ * @param {Bytes<32>} msgHash - The digest the signers approved.
137
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
138
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - Signatures over `msgHash`.
139
+ * @returns {[]} Empty tuple.
140
+ */
141
+ export circuit assertApprovals(
142
+ msgHash: Bytes<32>,
143
+ pubkeys: Vector<2, Secp256k1Point>,
144
+ signatures: Vector<2, Secp256k1EcdsaSignature>
145
+ ): [] {
146
+ const initialState = VerificationState {
147
+ validCount: 0 as Uint<8>,
148
+ prevCommitment: pad(32, ""),
149
+ msgHash: msgHash
150
+ };
151
+
152
+ const finalState = fold(verifySignature, initialState, pubkeys, signatures);
153
+ Signer_assertThresholdMet(finalState.validCount);
154
+ }
155
+
156
+ /**
157
+ * @description Fold callback. Verifies one signer's approval.
158
+ *
159
+ * @notice Verification goes through `Ecdsa`'s low-s wrapper, so the high-s
160
+ * encoding of an approval is rejected as an invalid signature.
161
+ *
162
+ * @dev Duplicates are caught by comparing against the previous commitment
163
+ * alone, which is exhaustive only at width 2. Widening the approval vectors
164
+ * requires an all-pairs check.
165
+ *
166
+ * @param {VerificationState} state - Accumulator threaded through fold.
167
+ * @param {Secp256k1Point} pubkey - The signer's secp256k1 public key.
168
+ * @param {Secp256k1EcdsaSignature} signature - The signer's signature over `msgHash`.
169
+ * @returns {VerificationState} Updated accumulator.
170
+ */
171
+ circuit verifySignature(
172
+ state: VerificationState,
173
+ pubkey: Secp256k1Point,
174
+ signature: Secp256k1EcdsaSignature
175
+ ): VerificationState {
176
+ const commitment = calculateSignerId(pubkey, _instanceSalt);
177
+
178
+ assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
179
+
180
+ Signer_assertSigner(commitment);
181
+
182
+ assert(
183
+ Ecdsa_secp256k1EcdsaVerifyLowS(state.msgHash, signature, pubkey),
184
+ "Multisig: invalid signature"
185
+ );
186
+
187
+ return VerificationState {
188
+ validCount: state.validCount + 1 as Uint<8>,
189
+ prevCommitment: commitment,
190
+ msgHash: state.msgHash
191
+ };
192
+ }
193
+
194
+ // ─── View ───────────────────────────────────────────────────────────────────
195
+
196
+ /**
197
+ * @description Returns the number of registered signers.
198
+ *
199
+ * @circuitInfo k=6, rows=34
200
+ *
201
+ * @returns {Uint<8>} The signer count.
202
+ */
203
+ export circuit getSignerCount(): Uint<8> {
204
+ return Signer_getSignerCount();
205
+ }
206
+
207
+ /**
208
+ * @description Returns the approval threshold.
209
+ *
210
+ * @circuitInfo k=6, rows=34
211
+ *
212
+ * @returns {Uint<8>} The threshold.
213
+ */
214
+ export circuit getThreshold(): Uint<8> {
215
+ return Signer_getThreshold();
216
+ }
217
+
218
+ /**
219
+ * @description Returns whether the given commitment is a registered signer.
220
+ *
221
+ * @circuitInfo k=8, rows=129
222
+ *
223
+ * @param {Bytes<32>} commitment - The signer commitment to check.
224
+ * @returns {Boolean} True if registered.
225
+ */
226
+ export circuit isSigner(commitment: Bytes<32>): Boolean {
227
+ return Signer_isSigner(commitment);
228
+ }
229
+ }