@openzeppelin/compact-contracts 0.3.0-rc.1 → 0.4.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 (46) hide show
  1. package/README.md +14 -4
  2. package/access/AccessControl.compact +14 -14
  3. package/access/Ownable.compact +16 -10
  4. package/access/ShieldedAccessControl.compact +26 -2
  5. package/access/ZOwnablePK.compact +9 -9
  6. package/crypto/EcdhMask.compact +5 -5
  7. package/crypto/Ecdsa.compact +111 -0
  8. package/crypto/Eip712.compact +168 -0
  9. package/crypto/ElGamal.compact +33 -107
  10. package/multisig/EcdsaSignerManager.compact +246 -0
  11. package/multisig/ForwarderPrivate.compact +12 -6
  12. package/multisig/ForwarderShielded.compact +3 -3
  13. package/multisig/ForwarderUnshielded.compact +3 -3
  14. package/multisig/ProposalManager.compact +195 -23
  15. package/multisig/ShieldedTreasury.compact +29 -2
  16. package/multisig/ShieldedTreasuryStateless.compact +2 -2
  17. package/multisig/Signer.compact +28 -80
  18. package/multisig/UnshieldedTreasury.compact +10 -2
  19. package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +4 -4
  20. package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +4 -4
  21. package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +4 -4
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
  25. package/multisig/presets/ShieldedMultiSigV2.compact +267 -259
  26. package/package.json +31 -25
  27. package/security/Allowlist.compact +6 -6
  28. package/security/Blocklist.compact +6 -6
  29. package/security/Initializable.compact +9 -5
  30. package/security/Pausable.compact +11 -7
  31. package/token/ConfidentialFungibleToken.compact +143 -270
  32. package/token/FungibleToken.compact +36 -22
  33. package/token/MultiToken.compact +16 -16
  34. package/token/NativeShieldedToken.compact +17 -66
  35. package/token/NativeShieldedTokenCore.compact +26 -48
  36. package/token/NativeShieldedTokenFamily.compact +17 -67
  37. package/token/NonFungibleToken.compact +40 -79
  38. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +12 -7
  39. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +3 -3
  40. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +8 -8
  41. package/token/extensions/NativeShieldedTokenPublicSupply.compact +8 -8
  42. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +6 -6
  43. package/utils/EvmAbi.compact +134 -0
  44. package/utils/Utils.compact +2 -12
  45. package/multisig/presets/ShieldedMultiSig.compact +0 -227
  46. package/multisig/presets/ShieldedMultiSigV3.compact +0 -368
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (crypto/Eip712.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module Eip712
8
+ * @description The EIP-712 typed-data envelope, for verifying signatures
9
+ * produced by Ethereum signing infrastructure.
10
+ *
11
+ * A typed-data signature covers `keccak256(0x1901 || domainSeparator ||
12
+ * structHash)`. The domain separator binds the signature to one application,
13
+ * so a signature obtained for any other domain cannot be replayed here — the
14
+ * property `personal_sign` does not provide, since it signs an opaque hash
15
+ * with no application binding.
16
+ *
17
+ * Build `structHash` from EVM ABI words (see `utils/EvmAbi`), with the
18
+ * operation's type hash as the first word.
19
+ *
20
+ * @notice The domain is `(name, version, salt)`. `chainId` and
21
+ * `verifyingContract` are omitted, which EIP-712 permits — every domain field
22
+ * is optional. They are omitted because neither is expressible: a Midnight
23
+ * contract address is 32 bytes and does not fit `verifyingContract`'s
24
+ * `address` type, and no network identifier is available in-circuit.
25
+ *
26
+ * On an EVM chain `verifyingContract` is what makes a domain identify one
27
+ * deployment rather than one application. Without it, a consumer has to get
28
+ * that binding elsewhere. There are two places to put it, and they are not
29
+ * equivalent:
30
+ *
31
+ * - As a field of the operation struct, holding the contract's own address.
32
+ * This is the stronger option and the one to reach for by default: a
33
+ * Midnight address is randomised at deployment and cannot be predicted, so
34
+ * no two instances share one, and a signature for one deployment cannot
35
+ * authorise another.
36
+ * - As `salt`, a per-deployment value in the domain. This is weaker, because
37
+ * it separates domains rather than messages. It is worth adding when the
38
+ * signer gates on domains but cannot read struct fields, since that is the
39
+ * only level at which it is visible.
40
+ *
41
+ * A consumer doing neither has one domain shared by every deployment of the
42
+ * contract, and nothing distinguishing them.
43
+ *
44
+ * @notice Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
45
+ */
46
+ module Eip712 {
47
+ import CompactStandardLibrary;
48
+
49
+ /**
50
+ * @description The 66-byte typed-data preimage: the two-byte prefix, the
51
+ * domain separator, then the struct hash. `Bytes<n>` is emitted at exactly
52
+ * `n` bytes, so this hashes as the concatenation an EVM signer produces.
53
+ */
54
+ struct TypedDataPreimage {
55
+ prefixBytes: Bytes<2>,
56
+ domainSeparator: Bytes<32>,
57
+ structHash: Bytes<32>
58
+ }
59
+
60
+ /**
61
+ * @description The four words of the domain separator's preimage.
62
+ */
63
+ struct DomainFields {
64
+ typeHash: Bytes<32>,
65
+ hashedName: Bytes<32>,
66
+ hashedVersion: Bytes<32>,
67
+ salt: Bytes<32>
68
+ }
69
+
70
+ /**
71
+ * @description `0x1901`, the EIP-191 version byte `0x01` behind the `0x19`
72
+ * marker, which is how EIP-712 identifies itself.
73
+ *
74
+ * @dev Written as byte values because the bytes are not printable. The
75
+ * version byte is what keeps a typed-data digest from colliding with a
76
+ * `personal_sign` one (version `0x45`) or an intended-validator one (`0x00`).
77
+ *
78
+ * @returns {Bytes<2>} The typed-data prefix.
79
+ */
80
+ pure circuit TYPED_DATA_PREFIX(): Bytes<2> {
81
+ const raw: Vector<2, Uint<8>> = [25, 1];
82
+
83
+ return raw as Bytes<2>;
84
+ }
85
+
86
+ /**
87
+ * @description `keccak256("EIP712Domain(string name,string version,bytes32 salt)")`.
88
+ *
89
+ * @dev Written as a literal word. `compactc` emits a full keccak
90
+ * permutation even for a constant argument, so hashing the type string
91
+ * in-circuit would roughly double `domainSeparator`. The spec checks this
92
+ * word against the type string above.
93
+ *
94
+ * The string must match the domain's field set exactly: a difference of one
95
+ * character yields a different separator and every signature fails.
96
+ *
97
+ * @returns {Bytes<32>} The domain type hash.
98
+ */
99
+ pure circuit DOMAIN_TYPEHASH(): Bytes<32> {
100
+ // keccak256("EIP712Domain(string name,string version,bytes32 salt)")
101
+ // = 0x599a80fcaa47b95e2323ab4d34d34e0cc9feda4b843edafcc30c7bdf60ea15bf
102
+ const typeHash: Vector<32, Uint<8>> = [
103
+ 0x59, 0x9a, 0x80, 0xfc, 0xaa, 0x47, 0xb9, 0x5e,
104
+ 0x23, 0x23, 0xab, 0x4d, 0x34, 0xd3, 0x4e, 0x0c,
105
+ 0xc9, 0xfe, 0xda, 0x4b, 0x84, 0x3e, 0xda, 0xfc,
106
+ 0xc3, 0x0c, 0x7b, 0xdf, 0x60, 0xea, 0x15, 0xbf
107
+ ];
108
+
109
+ return typeHash as Bytes<32>;
110
+ }
111
+
112
+ /**
113
+ * @description Computes the domain separator for `(name, version, salt)`.
114
+ *
115
+ * EIP-712 encodes a `string` field as the keccak hash of its UTF-8 bytes,
116
+ * so the name and version arrive already hashed. Compact has no dynamic
117
+ * types, so a consumer hashes its own fixed-length literals.
118
+ *
119
+ * @constraints k=14, rows=4561 (zkir-v3)
120
+ *
121
+ * @param {Bytes<32>} hashedName - `keccak256(bytes(name))`.
122
+ * @param {Bytes<32>} hashedVersion - `keccak256(bytes(version))`.
123
+ * @param {Bytes<32>} salt - Per-deployment, per-network random value.
124
+ * @returns {Bytes<32>} The domain separator.
125
+ */
126
+ export pure circuit domainSeparator(
127
+ hashedName: Bytes<32>,
128
+ hashedVersion: Bytes<32>,
129
+ salt: Bytes<32>
130
+ ): Bytes<32> {
131
+ return keccak256<DomainFields>(DomainFields {
132
+ typeHash: DOMAIN_TYPEHASH(),
133
+ hashedName: hashedName,
134
+ hashedVersion: hashedVersion,
135
+ salt: salt
136
+ });
137
+ }
138
+
139
+ /**
140
+ * @description Wraps a struct hash in the typed-data envelope, yielding the
141
+ * digest an EVM signer actually signs.
142
+ *
143
+ * @constraints k=14, rows=4463 (zkir-v3)
144
+ *
145
+ * Requirements:
146
+ *
147
+ * - `separator` must be non-zero, which an initialized domain always is.
148
+ *
149
+ * @param {Bytes<32>} separator - The domain separator, fixed at deployment.
150
+ * @param {Bytes<32>} structHash - `keccak256(typeHash || the operation's ABI words)`.
151
+ * @returns {Bytes<32>} The digest to verify signatures against.
152
+ */
153
+ export pure circuit hashTypedData(
154
+ separator: Bytes<32>,
155
+ structHash: Bytes<32>
156
+ ): Bytes<32> {
157
+ // A zero separator is what an uninitialized ledger cell reads as, and a
158
+ // digest built from one still verifies signatures. Fail closed rather than
159
+ // authorize an operation under an unset domain.
160
+ assert(separator != pad(32, ""), "Eip712: domain separator not set");
161
+
162
+ return keccak256<TypedDataPreimage>(TypedDataPreimage {
163
+ prefixBytes: TYPED_DATA_PREFIX(),
164
+ domainSeparator: separator,
165
+ structHash: structHash
166
+ });
167
+ }
168
+ }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-rc.1 (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (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,14 +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
36
  *
37
- * @dev Domain separation. `persistentHash<Vector<N, Bytes<32>>>` is NOT
38
- * domain-separating on its own. Two circuits hashing the same arity and
39
- * element type share one hash domain, and the hash cannot tell which slot was
40
- * "meant" as a tag. `secretToScalar` therefore prefixes a fixed constant, and
41
- * that constant is placed FIRST so no call to `expandRandomness` (whose two
42
- * slots are both caller-supplied) can reproduce the tuple by ordinary use.
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.
43
40
  *
44
41
  * @dev TRUST ASSUMPTION — subgroup membership. This is the module's most
45
42
  * load-bearing assumption. Every `JubjubPoint` reaching a curve operation is
@@ -50,9 +47,8 @@ pragma language_version >= 0.23.0;
50
47
  * off-curve / low-order / mixed-order point makes the circuit unsatisfiable.
51
48
  * The module therefore performs NO in-circuit subgroup check: the runtime
52
49
  * already constrains membership (so it is unnecessary), and the direct test
53
- * `ℓ*P == O` is unavailable anyway because `ecMul` by `ℓ` faults (verified — `ℓ`
54
- * exceeds the valid scalar range, while `ℓ-1` is accepted). The `ecNeg` identity
55
- * (`(ℓ-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.
56
52
  *
57
53
  * Basis (and its limits): source-verified against `midnight-circuits` 6.0.0 —
58
54
  * the version vendored by the Midnight onchain-runtime we compile against —
@@ -109,34 +105,11 @@ module ElGamal {
109
105
  * @description Maps a 32-byte secret to a valid Jubjub scalar. See the
110
106
  * module-level hash-to-scalar note for why `degradeToTransient` is required.
111
107
  *
112
- * @dev Domain separation here is load-bearing, not cosmetic. This output is
113
- * the private key protecting every ciphertext held under an account. Public
114
- * account identifiers derived from `Utils.computeAccountId`, for example,
115
- * are computed as `persistentHash([secretKey])`, and that identifier
116
- * is stored in the clear as a ledger map key. Untagged, this circuit
117
- * would compute that same hash, so a secret used in both roles would
118
- * have its encryption key recoverable from a published identifier by applying
119
- * `degradeToTransient`. The tag makes the two derivations unrelated, so a
120
- * wallet MAY safely derive its account secret and its encryption secret from
121
- * common key material.
122
- *
123
- * @dev The tag is FIRST, deliberately. `expandRandomness` hashes
124
- * `[seed, tag]` with both slots caller-supplied, so tagging this circuit as
125
- * `[secret, tag]` would make `secretToScalar(x)` identical to
126
- * `expandRandomness(x, <this tag>)` reachable by passing this domain string
127
- * to the parameter that exists to receive domain strings. Leading with the
128
- * tag means reproducing this output through `expandRandomness` would require
129
- * passing the domain constant as the *seed*. Do not normalise the orderings.
130
- *
131
108
  * @param secret - The 32-byte secret to map.
132
- * @return A Field guaranteed to be a valid Jubjub scalar.
109
+ * @return A valid Jubjub scalar.
133
110
  */
134
- export pure circuit secretToScalar(secret: Bytes<32>): Field {
135
- return degradeToTransient(
136
- persistentHash<Vector<2, Bytes<32>>>(
137
- [pad(32, "ElGamal:secretToScalar"), secret]
138
- )
139
- );
111
+ export pure circuit secretToScalar(secret: Bytes<32>): JubjubScalar {
112
+ return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret])) as JubjubScalar;
140
113
  }
141
114
 
142
115
  /**
@@ -159,49 +132,16 @@ module ElGamal {
159
132
  *
160
133
  * @param seed - The seed to expand.
161
134
  * @param tag - A 32-byte tag distinguishing this expansion from its siblings.
162
- * @return A Field suitable for use as encryption randomness.
135
+ * @return A scalar suitable for use as encryption randomness.
163
136
  */
164
- export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>): Field {
165
- 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;
166
139
  }
167
140
 
168
141
  // ---------------------------------------------------------------------------
169
142
  // Curve helpers
170
143
  // ---------------------------------------------------------------------------
171
144
 
172
- /**
173
- * @description The order of the Jubjub prime-order subgroup (`EmbeddedFr`
174
- * modulus `ℓ`), minus one. Used as the scalar for point negation: for any `P`
175
- * in the subgroup, `(ℓ - 1) * P = ℓ*P - P = O - P = -P`.
176
- *
177
- * @notice The negation identity this enables (`P + (ℓ-1)*P == O`) is exercised
178
- * by the `negate` round-trip tests. Compact has no module-level constants, so
179
- * the value is exposed as a pure circuit returning the literal.
180
- */
181
- pure circuit JUBJUB_SUBGROUP_ORDER_MINUS_ONE(): Field {
182
- return 6554484396890773809930967563523245729705921265872317281365359162392183254198 as Field;
183
- }
184
-
185
- /**
186
- * @description Negates a Jubjub point.
187
- *
188
- * @notice Implemented as scalar multiplication by `ℓ-1` (via
189
- * `JUBJUB_SUBGROUP_ORDER_MINUS_ONE`) rather than the obvious `ecMul(p, -1)`:
190
- * the scalar `-1` computed in the `Field` type is `BLS_modulus - 1`, which
191
- * exceeds the Jubjub scalar field order and faults `ecMul` ("failed to decode
192
- * for built-in type EmbeddedFr"). (Coordinate negation `(-x, y)` via
193
- * `constructJubjubPoint`/`jubjubPointX`/`jubjubPointY` is an equivalent
194
- * alternative that does not depend on the subgroup assumption.) The
195
- * `(ℓ-1)*P = -P` identity holds only in the prime-order subgroup, but that
196
- * always applies here: the runtime constrains every assigned `JubjubPoint`
197
- * into the prime-order subgroup (the embedded-curve gadget assigns points via
198
- * cofactor clearing), so no in-subgroup precondition needs to be checked or
199
- * assumed. See the module-level subgroup note.
200
- */
201
- pure circuit ecNeg(p: JubjubPoint): JubjubPoint {
202
- return ecMul(p, JUBJUB_SUBGROUP_ORDER_MINUS_ONE());
203
- }
204
-
205
145
  // ---------------------------------------------------------------------------
206
146
  // Encryption & homomorphic operations
207
147
  // ---------------------------------------------------------------------------
@@ -216,7 +156,7 @@ module ElGamal {
216
156
  * before treating it as private.
217
157
  */
218
158
  export pure circuit encryptZero(): Ciphertext {
219
- const id = ecMulGenerator(0 as Field);
159
+ const id = ecMulGenerator(0 as JubjubScalar);
220
160
  return Ciphertext { c1: id, c2: id };
221
161
  }
222
162
 
@@ -244,9 +184,9 @@ module ElGamal {
244
184
  * @param r - The encryption randomness.
245
185
  * @return The ciphertext `(g^r, pk^r * m)`.
246
186
  */
247
- export pure circuit encryptPoint(pk: JubjubPoint, m: JubjubPoint, r: Field): Ciphertext {
248
- assert(pk != ecMulGenerator(0 as Field), "ElGamal: identity pk");
249
- 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");
250
190
  const c1 = ecMulGenerator(r);
251
191
  const mask = ecMul(pk, r);
252
192
  const c2 = ecAdd(mask, m);
@@ -256,8 +196,8 @@ module ElGamal {
256
196
  /**
257
197
  * @description Produces a fresh ciphertext of `value` under `pk` with
258
198
  * randomness `r`, lifting `value` to the point `g^value`. Randomness is taken
259
- * as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion
260
- * 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.
261
201
  *
262
202
  * Requirements:
263
203
  *
@@ -269,8 +209,8 @@ module ElGamal {
269
209
  * @param r - The encryption randomness.
270
210
  * @return The ciphertext `(g^r, pk^r * g^value)`.
271
211
  */
272
- export pure circuit encrypt(pk: JubjubPoint, value: Uint<128>, r: Field): Ciphertext {
273
- 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);
274
214
  }
275
215
 
276
216
  /**
@@ -340,7 +280,7 @@ module ElGamal {
340
280
  * @param k - The public scalar multiplier (a valid Jubjub scalar).
341
281
  * @return A ciphertext of `k * v`.
342
282
  */
343
- export pure circuit scalarMul(ct: Ciphertext, k: Field): Ciphertext {
283
+ export pure circuit scalarMul(ct: Ciphertext, k: JubjubScalar): Ciphertext {
344
284
  return Ciphertext { c1: ecMul(ct.c1, k), c2: ecMul(ct.c2, k) };
345
285
  }
346
286
 
@@ -359,7 +299,7 @@ module ElGamal {
359
299
  old: Ciphertext,
360
300
  pk: JubjubPoint,
361
301
  value: Uint<128>,
362
- r: Field
302
+ r: JubjubScalar
363
303
  ): Ciphertext {
364
304
  return add(old, encrypt(pk, value, r));
365
305
  }
@@ -382,7 +322,7 @@ module ElGamal {
382
322
  old: Ciphertext,
383
323
  pk: JubjubPoint,
384
324
  value: Uint<128>,
385
- r: Field
325
+ r: JubjubScalar
386
326
  ): Ciphertext {
387
327
  return sub(old, encrypt(pk, value, r));
388
328
  }
@@ -410,7 +350,7 @@ module ElGamal {
410
350
  * @param r - Fresh, nonzero randomness for the rerandomization.
411
351
  * @return A fresh ciphertext of the same plaintext.
412
352
  */
413
- export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: Field): Ciphertext {
353
+ export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: JubjubScalar): Ciphertext {
414
354
  return add(ct, encrypt(pk, 0, r));
415
355
  }
416
356
 
@@ -458,17 +398,14 @@ module ElGamal {
458
398
  m: JubjubPoint
459
399
  ): [] {
460
400
  // Inlines the same key check as `assertKeyPair` (rather than calling it) so
461
- // `ekField` is computed once and reused for the decryption below, avoiding
401
+ // `ekScalar` is computed once and reused for the decryption below, avoiding
462
402
  // a second `secretToScalar` hash in this hot verification path.
463
- const ekField = secretToScalar(ek);
464
- assert(ecMulGenerator(ekField) == pk,
403
+ const ekScalar = secretToScalar(ek);
404
+ assert(ecMulGenerator(ekScalar) == pk,
465
405
  "ElGamal: ek/pk mismatch");
466
406
 
467
- // m = c2 - ek*c1. ek*c1 is negated via ecNeg (scalar mul by ℓ-1) rather
468
- // than ecMul by a negative scalar, which would exceed the Jubjub scalar
469
- // field. ekField < ℓ (the subgroup order), so ecMul(ct.c1, ekField) is a
470
- // valid scalar mul.
471
- const ekC1 = ecMul(ct.c1, ekField);
407
+ // m = c2 - ek*c1, with ek*c1 negated via ecNeg.
408
+ const ekC1 = ecMul(ct.c1, ekScalar);
472
409
  const mPoint = ecAdd(ct.c2, ecNeg(ekC1));
473
410
  assert(mPoint == m,
474
411
  "ElGamal: plaintext mismatch");
@@ -482,17 +419,6 @@ module ElGamal {
482
419
  * wrong plaintext fails the decryption check. Lifted special case of
483
420
  * `assertDecryptsToPoint` with the message point `g^claimedValue`.
484
421
  *
485
- * @notice `claimedValue` is the widest `Uint` Compact offers, so this parameter
486
- * never bounds a caller. A narrower value costs proportionally fewer rows: the
487
- * range constraint belongs to where the value is declared, not to this
488
- * signature.
489
- *
490
- * @warning The claim domain must stay below the Jubjub subgroup order `l`:
491
- * verification compares `g^claimedValue`, so two claims congruent mod `l` are
492
- * indistinguishable. `Uint<248>` clears `l` (~2^251.86) by under 4 bits, and
493
- * that width is set by Compact's cap rather than by this bound. 251 is the
494
- * widest sound `Uint` if the cap ever rises.
495
- *
496
422
  * Requirements:
497
423
  *
498
424
  * - `ek` is the secret for `pk`.
@@ -507,8 +433,8 @@ module ElGamal {
507
433
  ct: Ciphertext,
508
434
  pk: JubjubPoint,
509
435
  ek: Bytes<32>,
510
- claimedValue: Uint<248>
436
+ claimedValue: Uint<128>
511
437
  ): [] {
512
- assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as Field));
438
+ assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as JubjubScalar));
513
439
  }
514
440
  }
@@ -0,0 +1,246 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (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 and configures it
17
+ * from `initialize`. Consuming contracts own the digest: they compute the
18
+ * message hash their signers signed, then hand it to `assertApprovals` with
19
+ * the parallel public-key and signature vectors.
20
+ *
21
+ * @notice Consuming contracts must not import `Signer` alongside this module.
22
+ * A second import instantiates a separate registry that `assertApprovals`
23
+ * never reads.
24
+ */
25
+ module EcdsaSignerManager {
26
+ import CompactStandardLibrary;
27
+ import "../crypto/Ecdsa" prefix Ecdsa_;
28
+ import "./Signer"<Bytes<32>> prefix Signer_;
29
+
30
+ // Named import/re-export of the registry state only, so a consuming
31
+ // contract's ledger keys read as `_signers` rather than `Signer__signers`.
32
+ // The circuits stay behind the `Signer_` prefix.
33
+ import { _signers, _signerCount, _threshold } from "./Signer"<Bytes<32>>;
34
+ export { _signers, _signerCount, _threshold };
35
+
36
+ // ─── State ──────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * @description Deployment-supplied salt for signer commitments. Set at
40
+ * initialization and never changed. The registered commitments must have been
41
+ * derived under this same salt.
42
+ */
43
+ export sealed ledger _instanceSalt: Bytes<32>;
44
+
45
+ // ─── Types ──────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * @description Input to `persistentHash` for computing signer commitments.
49
+ */
50
+ struct SignerCommitmentInput {
51
+ pkX: Bytes<32>,
52
+ pkY: Bytes<32>,
53
+ salt: Bytes<32>,
54
+ domain: Bytes<32>
55
+ }
56
+
57
+ /**
58
+ * @description Accumulator threaded through the approval fold: the running
59
+ * valid count, the previous commitment (for duplicate detection), and the
60
+ * digest every signature is checked against.
61
+ */
62
+ struct VerificationState {
63
+ validCount: Uint<8>,
64
+ prevCommitment: Bytes<32>,
65
+ msgHash: Bytes<32>
66
+ }
67
+
68
+ // ─── Initialization ─────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * @description Stores the instance salt and registers the initial signer set.
72
+ * Call from the consuming contract's constructor only. Writes the `sealed`
73
+ * salt, so it cannot be reached from an exported circuit.
74
+ *
75
+ * Requirements:
76
+ *
77
+ * - Can only be called once.
78
+ * - `instanceSalt` should be cryptographically random.
79
+ * - `signerCommitments` must not contain duplicates.
80
+ * - `threshold` must be non-zero and must not exceed `n`.
81
+ * - `threshold` must not exceed 2, the number of approvals `assertApprovals`
82
+ * verifies.
83
+ *
84
+ * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
85
+ * @param {Vector<n, Bytes<32>>} signerCommitments - Hashed signer identities.
86
+ * @param {Uint<8>} threshold - Minimum approvals required.
87
+ * @returns {[]} Empty tuple.
88
+ */
89
+ export circuit initialize<#n>(
90
+ instanceSalt: Bytes<32>,
91
+ signerCommitments: Vector<n, Bytes<32>>,
92
+ threshold: Uint<8>
93
+ ): [] {
94
+ assert(Signer_getSignerCount() == 0, "EcdsaSignerManager: signers already registered");
95
+ _instanceSalt = disclose(instanceSalt);
96
+ assert(threshold <= 2, "EcdsaSignerManager: threshold cannot exceed 2 (assertApprovals verifies 2 signatures)");
97
+
98
+ for (const commitment of signerCommitments) {
99
+ Signer__addSigner(commitment);
100
+ }
101
+ Signer__changeThreshold(threshold);
102
+ }
103
+
104
+ // ─── Commitments ────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * @description Computes a signer commitment from a secp256k1 public key,
108
+ * as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
109
+ *
110
+ * Pure, so the deployer can derive constructor commitments off-chain the
111
+ * same way the circuits derive them in-circuit.
112
+ *
113
+ * Requirements:
114
+ *
115
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
116
+ *
117
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
118
+ * @param {Bytes<32>} salt - The instance salt.
119
+ * @returns {Bytes<32>} The signer commitment.
120
+ */
121
+ export pure circuit calculateSignerId(
122
+ pk: Secp256k1Point,
123
+ salt: Bytes<32>
124
+ ): Bytes<32> {
125
+ return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
126
+ pkX: secp256k1PointX(pk) as Bytes<32>,
127
+ pkY: secp256k1PointY(pk) as Bytes<32>,
128
+ salt: salt,
129
+ domain: pad(32, "multisig:signer:")
130
+ });
131
+ }
132
+
133
+ // ─── Approvals ──────────────────────────────────────────────────────────────
134
+
135
+ /**
136
+ * @description Verifies two approvals over `msgHash` and asserts the
137
+ * threshold is met.
138
+ *
139
+ * `pubkeys` and `signatures` are parallel: entry `i` of each belongs to the
140
+ * same signer.
141
+ *
142
+ * @dev Both entries are asserted, so the count handed to the threshold check
143
+ * is always 2 and a threshold below 2 admits nothing extra.
144
+ *
145
+ * @constraints k=17, rows=64916 (zkir-v3)
146
+ *
147
+ * Requirements:
148
+ *
149
+ * - Every public key must hash to a registered signer commitment.
150
+ * - Every signature must be valid over `msgHash` and use the low-s encoding.
151
+ * - Signers must not be duplicates.
152
+ *
153
+ * @param {Bytes<32>} msgHash - The digest the signers approved.
154
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
155
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - Signatures over `msgHash`.
156
+ * @returns {[]} Empty tuple.
157
+ */
158
+ export circuit assertApprovals(
159
+ msgHash: Bytes<32>,
160
+ pubkeys: Vector<2, Secp256k1Point>,
161
+ signatures: Vector<2, Secp256k1EcdsaSignature>
162
+ ): [] {
163
+ const initialState = VerificationState {
164
+ validCount: 0 as Uint<8>,
165
+ prevCommitment: pad(32, ""),
166
+ msgHash: msgHash
167
+ };
168
+
169
+ const finalState = fold(verifySignature, initialState, pubkeys, signatures);
170
+ Signer_assertThresholdMet(finalState.validCount);
171
+ }
172
+
173
+ /**
174
+ * @description Fold callback. Verifies one signer's approval.
175
+ *
176
+ * @notice Verification goes through `Ecdsa`'s low-s wrapper, so the high-s
177
+ * encoding of an approval is rejected as an invalid signature.
178
+ *
179
+ * @dev Duplicates are caught by comparing against the previous commitment
180
+ * alone, which is exhaustive only at width 2. Widening the approval vectors
181
+ * requires an all-pairs check.
182
+ *
183
+ * @param {VerificationState} state - Accumulator threaded through fold.
184
+ * @param {Secp256k1Point} pubkey - The signer's secp256k1 public key.
185
+ * @param {Secp256k1EcdsaSignature} signature - The signer's signature over `msgHash`.
186
+ * @returns {VerificationState} Updated accumulator.
187
+ */
188
+ circuit verifySignature(
189
+ state: VerificationState,
190
+ pubkey: Secp256k1Point,
191
+ signature: Secp256k1EcdsaSignature
192
+ ): VerificationState {
193
+ const commitment = calculateSignerId(pubkey, _instanceSalt);
194
+
195
+ assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
196
+
197
+ Signer_assertSigner(commitment);
198
+
199
+ assert(
200
+ Ecdsa_secp256k1EcdsaVerifyLowS(state.msgHash, signature, pubkey),
201
+ "Multisig: invalid signature"
202
+ );
203
+
204
+ return VerificationState {
205
+ validCount: state.validCount + 1 as Uint<8>,
206
+ prevCommitment: commitment,
207
+ msgHash: state.msgHash
208
+ };
209
+ }
210
+
211
+ // ─── View ───────────────────────────────────────────────────────────────────
212
+
213
+ /**
214
+ * @description Returns the number of registered signers.
215
+ *
216
+ * @constraints k=6, rows=34 (zkir-v3)
217
+ *
218
+ * @returns {Uint<8>} The signer count.
219
+ */
220
+ export circuit getSignerCount(): Uint<8> {
221
+ return Signer_getSignerCount();
222
+ }
223
+
224
+ /**
225
+ * @description Returns the approval threshold.
226
+ *
227
+ * @constraints k=6, rows=34 (zkir-v3)
228
+ *
229
+ * @returns {Uint<8>} The threshold.
230
+ */
231
+ export circuit getThreshold(): Uint<8> {
232
+ return Signer_getThreshold();
233
+ }
234
+
235
+ /**
236
+ * @description Returns whether the given commitment is a registered signer.
237
+ *
238
+ * @constraints k=8, rows=129 (zkir-v3)
239
+ *
240
+ * @param {Bytes<32>} commitment - The signer commitment to check.
241
+ * @returns {Boolean} True if registered.
242
+ */
243
+ export circuit isSigner(commitment: Bytes<32>): Boolean {
244
+ return Signer_isSigner(commitment);
245
+ }
246
+ }