@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.
- package/README.md +10 -4
- package/access/AccessControl.compact +2 -2
- package/access/Ownable.compact +2 -2
- package/access/ShieldedAccessControl.compact +2 -2
- package/access/ZOwnablePK.compact +2 -2
- package/crypto/EcdhMask.compact +5 -5
- package/crypto/Ecdsa.compact +111 -0
- package/crypto/ElGamal.compact +33 -66
- package/multisig/EcdsaSignerManager.compact +229 -0
- package/multisig/ForwarderPrivate.compact +3 -3
- package/multisig/ForwarderShielded.compact +3 -3
- package/multisig/ForwarderUnshielded.compact +3 -3
- package/multisig/ProposalManager.compact +36 -2
- package/multisig/ShieldedTreasury.compact +35 -5
- package/multisig/ShieldedTreasuryStateless.compact +47 -18
- package/multisig/Signer.compact +46 -42
- package/multisig/UnshieldedTreasury.compact +45 -18
- package/multisig/presets/ShieldedMultiSig.compact +5 -5
- package/multisig/presets/ShieldedMultiSigV2.compact +72 -127
- package/multisig/presets/ShieldedMultiSigV3.compact +83 -140
- package/multisig/presets/forwarder/ForwarderPrivate.compact +2 -2
- package/multisig/presets/forwarder/ForwarderShielded.compact +2 -2
- package/multisig/presets/forwarder/ForwarderUnshielded.compact +2 -2
- package/package.json +22 -20
- package/security/Allowlist.compact +2 -2
- package/security/Blocklist.compact +2 -2
- package/security/Initializable.compact +2 -2
- package/security/Pausable.compact +2 -2
- package/token/ConfidentialFungibleToken.compact +46 -18
- package/token/FungibleToken.compact +2 -2
- package/token/MultiToken.compact +2 -2
- package/token/NativeShieldedToken.compact +3 -3
- package/token/NativeShieldedTokenCore.compact +2 -2
- package/token/NativeShieldedTokenFamily.compact +3 -3
- package/token/NonFungibleToken.compact +2 -2
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +2 -2
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
- package/token/extensions/{NativeShieldedTokenFamilySupply.compact → NativeShieldedTokenFamilyPublicSupply.compact} +10 -10
- package/token/extensions/{NativeShieldedTokenSupply.compact → NativeShieldedTokenPublicSupply.compact} +10 -10
- package/token/extensions/{NativeShieldedTokenSupplyCore.compact → NativeShieldedTokenPublicSupplyCore.compact} +10 -10
- package/utils/Utils.compact +2 -2
- package/multisig/SignerManager.compact +0 -205
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://docs.midnight.network/relnotes/compact/)
|
|
2
2
|
[](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.
|
|
41
|
-
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.
|
|
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.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/AccessControl.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module AccessControl
|
package/access/Ownable.compact
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/Ownable.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 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.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/ShieldedAccessControl.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 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.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (access/ZOwnablePK.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module ZOwnablePK
|
package/crypto/EcdhMask.compact
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (crypto/EcdhMask.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 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:
|
|
145
|
+
e: JubjubScalar,
|
|
146
146
|
domain: Bytes<32>
|
|
147
147
|
): Ciphertext {
|
|
148
|
-
const identity = ecMulGenerator(0 as
|
|
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:
|
|
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
|
+
}
|
package/crypto/ElGamal.compact
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (crypto/ElGamal.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 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
|
|
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 `ℓ`
|
|
47
|
-
*
|
|
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
|
|
109
|
+
* @return A valid Jubjub scalar.
|
|
107
110
|
*/
|
|
108
|
-
export pure circuit secretToScalar(secret: Bytes<32>):
|
|
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
|
|
135
|
+
* @return A scalar suitable for use as encryption randomness.
|
|
133
136
|
*/
|
|
134
|
-
export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>):
|
|
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
|
|
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:
|
|
218
|
-
assert(pk != ecMulGenerator(0 as
|
|
219
|
-
assert(r != 0 as
|
|
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
|
|
230
|
-
*
|
|
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:
|
|
243
|
-
return encryptPoint(pk, ecMulGenerator(value as
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
// `
|
|
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
|
|
434
|
-
assert(ecMulGenerator(
|
|
403
|
+
const ekScalar = secretToScalar(ek);
|
|
404
|
+
assert(ecMulGenerator(ekScalar) == pk,
|
|
435
405
|
"ElGamal: ek/pk mismatch");
|
|
436
406
|
|
|
437
|
-
// m = c2 - ek*c1
|
|
438
|
-
|
|
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
|
|
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
|
+
}
|