@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.
- package/README.md +14 -4
- package/access/AccessControl.compact +14 -14
- package/access/Ownable.compact +16 -10
- package/access/ShieldedAccessControl.compact +26 -2
- package/access/ZOwnablePK.compact +9 -9
- package/crypto/EcdhMask.compact +5 -5
- package/crypto/Ecdsa.compact +111 -0
- package/crypto/Eip712.compact +168 -0
- package/crypto/ElGamal.compact +33 -107
- package/multisig/EcdsaSignerManager.compact +246 -0
- package/multisig/ForwarderPrivate.compact +12 -6
- package/multisig/ForwarderShielded.compact +3 -3
- package/multisig/ForwarderUnshielded.compact +3 -3
- package/multisig/ProposalManager.compact +195 -23
- package/multisig/ShieldedTreasury.compact +29 -2
- package/multisig/ShieldedTreasuryStateless.compact +2 -2
- package/multisig/Signer.compact +28 -80
- package/multisig/UnshieldedTreasury.compact +10 -2
- package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +4 -4
- package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +4 -4
- package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +4 -4
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
- package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
- package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +267 -259
- package/package.json +31 -25
- package/security/Allowlist.compact +6 -6
- package/security/Blocklist.compact +6 -6
- package/security/Initializable.compact +9 -5
- package/security/Pausable.compact +11 -7
- package/token/ConfidentialFungibleToken.compact +143 -270
- package/token/FungibleToken.compact +36 -22
- package/token/MultiToken.compact +16 -16
- package/token/NativeShieldedToken.compact +17 -66
- package/token/NativeShieldedTokenCore.compact +26 -48
- package/token/NativeShieldedTokenFamily.compact +17 -67
- package/token/NonFungibleToken.compact +40 -79
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +12 -7
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +3 -3
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +8 -8
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +8 -8
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +6 -6
- package/utils/EvmAbi.compact +134 -0
- package/utils/Utils.compact +2 -12
- package/multisig/presets/ShieldedMultiSig.compact +0 -227
- 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
|
+
}
|
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.2 (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,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
|
|
35
|
+
* occasionally exceed the Jubjub scalar field order and be rejected.
|
|
36
36
|
*
|
|
37
|
-
* @dev
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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 `ℓ`
|
|
54
|
-
*
|
|
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
|
|
109
|
+
* @return A valid Jubjub scalar.
|
|
133
110
|
*/
|
|
134
|
-
export pure circuit secretToScalar(secret: Bytes<32>):
|
|
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
|
|
135
|
+
* @return A scalar suitable for use as encryption randomness.
|
|
163
136
|
*/
|
|
164
|
-
export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>):
|
|
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
|
|
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:
|
|
248
|
-
assert(pk != ecMulGenerator(0 as
|
|
249
|
-
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");
|
|
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
|
|
260
|
-
*
|
|
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:
|
|
273
|
-
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);
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
// `
|
|
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
|
|
464
|
-
assert(ecMulGenerator(
|
|
403
|
+
const ekScalar = secretToScalar(ek);
|
|
404
|
+
assert(ecMulGenerator(ekScalar) == pk,
|
|
465
405
|
"ElGamal: ek/pk mismatch");
|
|
466
406
|
|
|
467
|
-
// m = c2 - ek*c1
|
|
468
|
-
|
|
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<
|
|
436
|
+
claimedValue: Uint<128>
|
|
511
437
|
): [] {
|
|
512
|
-
assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as
|
|
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
|
+
}
|