@openzeppelin/compact-contracts 0.4.0-alpha.4 → 0.4.0-alpha.6
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 +1 -1
- package/access/AccessControl.compact +1 -1
- package/access/Ownable.compact +1 -1
- package/access/ShieldedAccessControl.compact +1 -1
- package/access/ZOwnablePK.compact +13 -4
- package/crypto/EcdhMask.compact +1 -1
- package/crypto/Ecdsa.compact +1 -1
- package/crypto/Eip712.compact +1 -1
- package/crypto/ElGamal.compact +1 -1
- package/multisig/ForwarderPrivate.compact +1 -1
- package/multisig/ForwarderShielded.compact +14 -5
- package/multisig/ForwarderUnshielded.compact +1 -1
- package/multisig/ProposalManager.compact +1 -1
- package/multisig/Secp256k1EcdsaMultisig.compact +275 -0
- package/multisig/ShieldedTreasury.compact +1 -1
- package/multisig/ShieldedTreasuryStateless.compact +1 -1
- package/multisig/Signer.compact +1 -1
- package/multisig/UnshieldedTreasury.compact +5 -3
- package/multisig/examples/ForwarderPrivateExample.compact +1 -1
- package/multisig/examples/ForwarderShieldedExample.compact +8 -5
- package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +10 -8
- package/multisig/examples/ShieldedMultiSigV2Example.compact +6 -8
- package/multisig/presets/NativeShieldedTokenIssuer.compact +126 -108
- package/multisig/presets/ShieldedMultiSigV2.compact +46 -45
- package/package.json +2 -2
- package/security/Allowlist.compact +1 -1
- package/security/Blocklist.compact +1 -1
- package/security/Initializable.compact +1 -1
- package/security/Pausable.compact +1 -1
- package/token/ConfidentialFungibleToken.compact +17 -1
- package/token/FungibleToken.compact +1 -1
- package/token/MultiToken.compact +1 -1
- package/token/NativeShieldedToken.compact +2 -2
- package/token/NativeShieldedTokenCore.compact +11 -18
- package/token/NativeShieldedTokenFamily.compact +2 -2
- package/token/NonFungibleToken.compact +1 -1
- package/token/examples/ZOwnableNativeShieldedTokenExample.compact +109 -0
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
- package/token/presets/ZOwnableNativeShieldedToken.compact +314 -0
- package/utils/EvmAbi.compact +1 -1
- package/utils/Utils.compact +1 -1
- package/multisig/EcdsaSignerManager.compact +0 -246
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.4 (token/presets/ZOwnableNativeShieldedToken.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.26.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ZOwnableNativeShieldedToken
|
|
8
|
+
* @description
|
|
9
|
+
* A native shielded token with a single shielded owner. Only the owner mints
|
|
10
|
+
* (to a coin public key, with a caller-supplied nonce) and burns (a coin the
|
|
11
|
+
* owner's wallet pays into the tx). Minted amounts are counted on-chain.
|
|
12
|
+
*
|
|
13
|
+
* Composes `ZOwnablePK` as `ZOwnablePK_` (owner commitment),
|
|
14
|
+
* `NativeShieldedToken` as `NST_` (one token color) and
|
|
15
|
+
* `NativeShieldedTokenPublicSupply` as `NSTPublicSupply_` (minted total).
|
|
16
|
+
*
|
|
17
|
+
* @dev Derivations:
|
|
18
|
+
* - `H<T>(...)`: `persistentHash<T>`.
|
|
19
|
+
* - Owner id: `id = H<Vector<2, Bytes<32>>>([pk.bytes, secretNonce])`.
|
|
20
|
+
* - Owner commitment: `H<Vector<4, Bytes<32>>>([id, instanceSalt,
|
|
21
|
+
* counter as Field as Bytes<32>, pad(32, "ZOwnablePK:shield:")])`.
|
|
22
|
+
* - Token color: `tokenType(_domain, kernel.self())`.
|
|
23
|
+
*
|
|
24
|
+
* @notice Implementing contracts call `initialize` from their constructor and
|
|
25
|
+
* MUST NOT import the composed modules directly.
|
|
26
|
+
*
|
|
27
|
+
* @notice Privacy:
|
|
28
|
+
* - Public, per tx: every mint amount (the Zswap mint effect and
|
|
29
|
+
* `_totalMinted`), the coin commitments and nullifiers, and which circuit
|
|
30
|
+
* ran.
|
|
31
|
+
* - Public, as state: `_ownerCommitment`, `_counter` (the number of ownership
|
|
32
|
+
* changes), `_instanceSalt`, `_totalMinted`, `_domain`, `_name`, `_symbol`,
|
|
33
|
+
* `_decimals`.
|
|
34
|
+
* - Private: mint `recipient` and `nonce`. Burn `amount`, `coin` and
|
|
35
|
+
* `refundTo`, as long as the observer does not know the coin's nonce. A change
|
|
36
|
+
* output reveals that a burn was partial.
|
|
37
|
+
* - Owner: stored as a commitment to `id`; the key and nonce never leave the
|
|
38
|
+
* proof.
|
|
39
|
+
*
|
|
40
|
+
* @notice Security:
|
|
41
|
+
* - Owner check: proves knowledge of a `(pk, secretNonce)` whose commitment
|
|
42
|
+
* matches. `ownPublicKey()` is prover-supplied, so the check does not bind
|
|
43
|
+
* the submitting wallet; `secretNonce` is the only secret.
|
|
44
|
+
* - Every mutating circuit checks ownership before any token check.
|
|
45
|
+
* - `transferOwnership` is one step; a wrong `newOwnerId` loses the contract.
|
|
46
|
+
* - `renounceOwnership` stops mint and burn forever.
|
|
47
|
+
* - Upgrades: the CMA can replace any circuit's verifier key, so it sits
|
|
48
|
+
* above the owner and should not be a single key.
|
|
49
|
+
* - A failed assert rejects the whole tx; no state moves.
|
|
50
|
+
*
|
|
51
|
+
* @notice Concurrency:
|
|
52
|
+
* - `mint` reads and writes `_totalMinted`; the read pins it, so at most one
|
|
53
|
+
* mint lands per block.
|
|
54
|
+
* - `burn` only reads owner state: burns commute with each other and with one
|
|
55
|
+
* mint.
|
|
56
|
+
* - `transferOwnership` / `renounceOwnership` write the owner commitment and
|
|
57
|
+
* conflict with every owner-gated call and `owner()` in the same block.
|
|
58
|
+
* - Metadata views and `totalMinted` read no owner state; `totalMinted`
|
|
59
|
+
* submitted as a tx conflicts with a same-block mint.
|
|
60
|
+
* - A tx that loses a same-block race fails and must be re-proven against
|
|
61
|
+
* the new state.
|
|
62
|
+
*
|
|
63
|
+
* @notice Scope:
|
|
64
|
+
* Mint and burn only. Holders transfer through Zswap directly. No roles,
|
|
65
|
+
* freeze, seize, contract-held coins, burned total or total supply.
|
|
66
|
+
*
|
|
67
|
+
* @notice Client-side duties:
|
|
68
|
+
* - Build every owner tx with the coin public key in the current `ownerId`
|
|
69
|
+
* and the matching `wit_secretNonce`. A wrong nonce fails the ownership
|
|
70
|
+
* check and reveals nothing.
|
|
71
|
+
* - `ownPublicKey()` is supplied by the client like a witness; the circuit
|
|
72
|
+
* cannot check that it belongs to the submitting wallet. The client is
|
|
73
|
+
* responsible for supplying the coin public key that is in the current
|
|
74
|
+
* `ownerId`; any other key fails the ownership check.
|
|
75
|
+
* - Compute `ownerId` off-chain with `_computeOwnerId`.
|
|
76
|
+
* - `nonce` must be fresh random bytes per mint.
|
|
77
|
+
* - Persist the coin infos `mint` and `burn` return. midnight-js encrypts an
|
|
78
|
+
* output to its recipient only when it can resolve the recipient's
|
|
79
|
+
* encryption key (its own, or one given in
|
|
80
|
+
* `additionalCoinEncPublicKeyMappings`).
|
|
81
|
+
* - Run the proof server where the owner controls it; it sees
|
|
82
|
+
* `wit_secretNonce`.
|
|
83
|
+
*
|
|
84
|
+
* @dev Composition:
|
|
85
|
+
* - Burn accounting: calling `NSTPublicSupply__addBurned` in `burn` would
|
|
86
|
+
* publish every burn amount.
|
|
87
|
+
*/
|
|
88
|
+
module ZOwnableNativeShieldedToken {
|
|
89
|
+
import CompactStandardLibrary;
|
|
90
|
+
|
|
91
|
+
import "../../access/ZOwnablePK" prefix ZOwnablePK_;
|
|
92
|
+
import "../NativeShieldedToken" prefix NST_;
|
|
93
|
+
import "../extensions/NativeShieldedTokenPublicSupply" prefix NSTPublicSupply_;
|
|
94
|
+
|
|
95
|
+
// Named import/re-export of the composed modules' state only, so an
|
|
96
|
+
// implementing contract can surface every ledger field under its own key.
|
|
97
|
+
// The circuits stay behind the prefixes above. The ownable flag is aliased
|
|
98
|
+
// because the token module already owns `_isInitialized`.
|
|
99
|
+
// `_totalBurned` is never written, so it stays empty.
|
|
100
|
+
import { _ownerCommitment, _counter, _instanceSalt, _isInitialized as _ownableIsInitialized } from "../../access/ZOwnablePK";
|
|
101
|
+
import { _domain, _name, _symbol, _decimals, _isInitialized } from "../NativeShieldedToken";
|
|
102
|
+
import { _totalMinted, _totalBurned } from "../extensions/NativeShieldedTokenPublicSupply";
|
|
103
|
+
|
|
104
|
+
export { _ownerCommitment, _counter, _instanceSalt, _ownableIsInitialized };
|
|
105
|
+
export { _domain, _name, _symbol, _decimals, _isInitialized };
|
|
106
|
+
export { _totalMinted, _totalBurned };
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @description Sets the owner and the token's domain and metadata.
|
|
110
|
+
*
|
|
111
|
+
* @notice Call from the implementing contract's constructor only. It writes
|
|
112
|
+
* sealed fields, so implementing contracts MUST NOT expose it as a
|
|
113
|
+
* post-deploy circuit.
|
|
114
|
+
*
|
|
115
|
+
* @warning `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin
|
|
116
|
+
* public key the owner's wallet will supply as `ownPublicKey()`. The circuit
|
|
117
|
+
* cannot check this at deploy; any other key leaves the contract with no
|
|
118
|
+
* usable owner and no recovery.
|
|
119
|
+
*
|
|
120
|
+
* Requirements:
|
|
121
|
+
*
|
|
122
|
+
* - Contract is not initialized.
|
|
123
|
+
* - `ownerId` is not zero.
|
|
124
|
+
*
|
|
125
|
+
* @param {Bytes<32>} ownerId - `_computeOwnerId(pk, secretNonce)` of the owner.
|
|
126
|
+
* @param {Bytes<32>} instanceSalt - Random per-deployment salt for the owner commitment.
|
|
127
|
+
* @param {Bytes<32>} tokenDomain - Domain of this contract's token color.
|
|
128
|
+
* @param {Opaque<"string">} name_ - The token name.
|
|
129
|
+
* @param {Opaque<"string">} symbol_ - The token symbol.
|
|
130
|
+
* @param {Uint<8>} decimals_ - The token decimals.
|
|
131
|
+
*/
|
|
132
|
+
export circuit initialize(
|
|
133
|
+
ownerId: Bytes<32>,
|
|
134
|
+
instanceSalt: Bytes<32>,
|
|
135
|
+
tokenDomain: Bytes<32>,
|
|
136
|
+
name_: Opaque<"string">,
|
|
137
|
+
symbol_: Opaque<"string">,
|
|
138
|
+
decimals_: Uint<8>,
|
|
139
|
+
): [] {
|
|
140
|
+
ZOwnablePK_initialize(ownerId, instanceSalt);
|
|
141
|
+
NST_initialize(tokenDomain, name_, symbol_, decimals_);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @description Mints `amount` to `recipient` and adds it to the minted total.
|
|
146
|
+
* Owner only.
|
|
147
|
+
*
|
|
148
|
+
* @constraints k=15, rows=21252
|
|
149
|
+
*
|
|
150
|
+
* Requirements:
|
|
151
|
+
*
|
|
152
|
+
* - Caller is the owner.
|
|
153
|
+
* - `recipient` is not zero.
|
|
154
|
+
*
|
|
155
|
+
* @param {ZswapCoinPublicKey} recipient - Coin public key that receives the coin.
|
|
156
|
+
* @param {Uint<64>} amount - Amount to mint.
|
|
157
|
+
* @param {Bytes<32>} nonce - Fresh random bytes per mint.
|
|
158
|
+
* @returns {ShieldedCoinInfo} The minted coin; persist it.
|
|
159
|
+
*/
|
|
160
|
+
export circuit mint(recipient: ZswapCoinPublicKey, amount: Uint<64>, nonce: Bytes<32>): ShieldedCoinInfo {
|
|
161
|
+
ZOwnablePK_assertOnlyOwner();
|
|
162
|
+
const coin = NST__mint(recipient, amount, nonce);
|
|
163
|
+
NSTPublicSupply__addMinted(amount);
|
|
164
|
+
return coin;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @description Burns `amount` of `coin`, paid into the tx by the caller's
|
|
169
|
+
* wallet, and refunds the rest to `refundTo`. Owner only.
|
|
170
|
+
*
|
|
171
|
+
* @constraints k=16, rows=51814
|
|
172
|
+
*
|
|
173
|
+
* Requirements:
|
|
174
|
+
*
|
|
175
|
+
* - Caller is the owner.
|
|
176
|
+
* - `coin.color` is this token's color.
|
|
177
|
+
* - `amount` <= `coin.value`.
|
|
178
|
+
* - `refundTo` is not zero.
|
|
179
|
+
*
|
|
180
|
+
* @param {ShieldedCoinInfo} coin - Coin of this token, paid into the tx by the caller's wallet.
|
|
181
|
+
* @param {Uint<64>} amount - Amount to burn from `coin`.
|
|
182
|
+
* @param {ZswapCoinPublicKey} refundTo - Coin public key that receives the change.
|
|
183
|
+
* @returns {Maybe<ShieldedCoinInfo>} The refund coin, or `none` on a full burn.
|
|
184
|
+
*/
|
|
185
|
+
export circuit burn(coin: ShieldedCoinInfo, amount: Uint<64>, refundTo: ZswapCoinPublicKey): Maybe<ShieldedCoinInfo> {
|
|
186
|
+
ZOwnablePK_assertOnlyOwner();
|
|
187
|
+
return NST__burn(coin, amount, refundTo);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @description Transfers ownership to `newOwnerId`. Owner only.
|
|
192
|
+
*
|
|
193
|
+
* @warning `newOwnerId` must be `_computeOwnerId(pk, secretNonce)` for the
|
|
194
|
+
* coin public key the new owner's wallet will supply as `ownPublicKey()`.
|
|
195
|
+
* The circuit cannot check this; any other key leaves the contract with no
|
|
196
|
+
* usable owner and no recovery.
|
|
197
|
+
*
|
|
198
|
+
* @constraints k=15, rows=17023
|
|
199
|
+
*
|
|
200
|
+
* Requirements:
|
|
201
|
+
*
|
|
202
|
+
* - Caller is the owner.
|
|
203
|
+
* - `newOwnerId` is not zero.
|
|
204
|
+
*
|
|
205
|
+
* @param {Bytes<32>} newOwnerId - `_computeOwnerId(pk, secretNonce)` of the new owner.
|
|
206
|
+
*/
|
|
207
|
+
export circuit transferOwnership(newOwnerId: Bytes<32>): [] {
|
|
208
|
+
ZOwnablePK_transferOwnership(newOwnerId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @description Clears the owner. Owner only.
|
|
213
|
+
*
|
|
214
|
+
* @warning Irreversible: mint and burn stop forever.
|
|
215
|
+
*
|
|
216
|
+
* @constraints k=14, rows=10579
|
|
217
|
+
*
|
|
218
|
+
* Requirements:
|
|
219
|
+
*
|
|
220
|
+
* - Caller is the owner.
|
|
221
|
+
*/
|
|
222
|
+
export circuit renounceOwnership(): [] {
|
|
223
|
+
ZOwnablePK_renounceOwnership();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @description Returns the current owner commitment.
|
|
228
|
+
*
|
|
229
|
+
* @constraints k=6, rows=48
|
|
230
|
+
*
|
|
231
|
+
* Requirements:
|
|
232
|
+
*
|
|
233
|
+
* - Contract is initialized.
|
|
234
|
+
*/
|
|
235
|
+
export circuit owner(): Bytes<32> {
|
|
236
|
+
return ZOwnablePK_owner();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @description Returns the owner id `H(pk, nonce)`.
|
|
241
|
+
*
|
|
242
|
+
* Requirements:
|
|
243
|
+
*
|
|
244
|
+
* - `pk` is a coin public key.
|
|
245
|
+
*
|
|
246
|
+
* @param {Either<ZswapCoinPublicKey, ContractAddress>} pk - The owner's coin public key.
|
|
247
|
+
* @param {Bytes<32>} nonce - The owner's secret nonce.
|
|
248
|
+
*/
|
|
249
|
+
export pure circuit _computeOwnerId(pk: Either<ZswapCoinPublicKey, ContractAddress>, nonce: Bytes<32>): Bytes<32> {
|
|
250
|
+
return ZOwnablePK__computeOwnerId(pk, nonce);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @description Returns the total amount ever minted.
|
|
255
|
+
*
|
|
256
|
+
* @constraints k=7, rows=63
|
|
257
|
+
*/
|
|
258
|
+
export circuit totalMinted(): Uint<128> {
|
|
259
|
+
return NSTPublicSupply_totalMinted();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @description Returns the token name.
|
|
264
|
+
*
|
|
265
|
+
* @constraints k=6, rows=26
|
|
266
|
+
*
|
|
267
|
+
* Requirements:
|
|
268
|
+
*
|
|
269
|
+
* - Contract is initialized.
|
|
270
|
+
*/
|
|
271
|
+
export circuit name(): Opaque<"string"> {
|
|
272
|
+
return NST_name();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @description Returns the token symbol.
|
|
277
|
+
*
|
|
278
|
+
* @constraints k=6, rows=26
|
|
279
|
+
*
|
|
280
|
+
* Requirements:
|
|
281
|
+
*
|
|
282
|
+
* - Contract is initialized.
|
|
283
|
+
*/
|
|
284
|
+
export circuit symbol(): Opaque<"string"> {
|
|
285
|
+
return NST_symbol();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @description Returns the token decimals.
|
|
290
|
+
*
|
|
291
|
+
* @constraints k=6, rows=26
|
|
292
|
+
*
|
|
293
|
+
* Requirements:
|
|
294
|
+
*
|
|
295
|
+
* - Contract is initialized.
|
|
296
|
+
*/
|
|
297
|
+
export circuit decimals(): Uint<8> {
|
|
298
|
+
return NST_decimals();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @description Returns this contract's token color,
|
|
303
|
+
* `tokenType(tokenDomain, kernel.self())`.
|
|
304
|
+
*
|
|
305
|
+
* @constraints k=13, rows=3962
|
|
306
|
+
*
|
|
307
|
+
* Requirements:
|
|
308
|
+
*
|
|
309
|
+
* - Contract is initialized.
|
|
310
|
+
*/
|
|
311
|
+
export circuit tokenColor(): Bytes<32> {
|
|
312
|
+
return NST_tokenColor();
|
|
313
|
+
}
|
|
314
|
+
}
|
package/utils/EvmAbi.compact
CHANGED
package/utils/Utils.compact
CHANGED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.4.0-alpha.4 (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
|
-
}
|