@openzeppelin/compact-contracts 0.3.0-alpha.1 → 0.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +10 -4
  2. package/access/AccessControl.compact +2 -2
  3. package/access/Ownable.compact +2 -2
  4. package/access/ShieldedAccessControl.compact +2 -2
  5. package/access/ZOwnablePK.compact +2 -2
  6. package/crypto/EcdhMask.compact +5 -5
  7. package/crypto/Ecdsa.compact +111 -0
  8. package/crypto/ElGamal.compact +33 -66
  9. package/multisig/EcdsaSignerManager.compact +229 -0
  10. package/multisig/ForwarderPrivate.compact +3 -3
  11. package/multisig/ForwarderShielded.compact +3 -3
  12. package/multisig/ForwarderUnshielded.compact +3 -3
  13. package/multisig/ProposalManager.compact +36 -2
  14. package/multisig/ShieldedTreasury.compact +35 -5
  15. package/multisig/ShieldedTreasuryStateless.compact +47 -18
  16. package/multisig/Signer.compact +46 -42
  17. package/multisig/UnshieldedTreasury.compact +45 -18
  18. package/multisig/presets/ShieldedMultiSig.compact +5 -5
  19. package/multisig/presets/ShieldedMultiSigV2.compact +72 -127
  20. package/multisig/presets/ShieldedMultiSigV3.compact +83 -140
  21. package/multisig/presets/forwarder/ForwarderPrivate.compact +2 -2
  22. package/multisig/presets/forwarder/ForwarderShielded.compact +2 -2
  23. package/multisig/presets/forwarder/ForwarderUnshielded.compact +2 -2
  24. package/package.json +22 -20
  25. package/security/Allowlist.compact +2 -2
  26. package/security/Blocklist.compact +2 -2
  27. package/security/Initializable.compact +2 -2
  28. package/security/Pausable.compact +2 -2
  29. package/token/ConfidentialFungibleToken.compact +46 -18
  30. package/token/FungibleToken.compact +2 -2
  31. package/token/MultiToken.compact +2 -2
  32. package/token/NativeShieldedToken.compact +3 -3
  33. package/token/NativeShieldedTokenCore.compact +2 -2
  34. package/token/NativeShieldedTokenFamily.compact +3 -3
  35. package/token/NonFungibleToken.compact +2 -2
  36. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +2 -2
  37. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
  38. package/token/extensions/{NativeShieldedTokenFamilySupply.compact → NativeShieldedTokenFamilyPublicSupply.compact} +10 -10
  39. package/token/extensions/{NativeShieldedTokenSupply.compact → NativeShieldedTokenPublicSupply.compact} +10 -10
  40. package/token/extensions/{NativeShieldedTokenSupplyCore.compact → NativeShieldedTokenPublicSupplyCore.compact} +10 -10
  41. package/utils/Utils.compact +2 -2
  42. package/multisig/SignerManager.compact +0 -205
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/ShieldedMultiSigV2.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/presets/ShieldedMultiSigV2.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @title ShieldedMultisigV2
@@ -22,38 +22,13 @@ pragma language_version >= 0.23.0;
22
22
 
23
23
  import CompactStandardLibrary;
24
24
 
25
+ import "../EcdsaSignerManager" prefix EcdsaSignerManager_;
25
26
  import "../ProposalManager" prefix Proposal_;
26
27
  import "../ShieldedTreasuryStateless" prefix Treasury_;
27
- import "../SignerManager"<Bytes<32>> prefix Signer_;
28
-
29
- // ─── Types ──────────────────────────────────────────────────────
30
-
31
- /**
32
- * @description Accumulator for fold-based signature verification.
33
- * Threads the valid count, previous commitment (for duplicate
34
- * detection), and message hash through each iteration.
35
- */
36
- export struct VerificationState {
37
- validCount: Uint<8>,
38
- prevCommitment: Bytes<32>,
39
- msgHash: Bytes<32>
40
- }
41
-
42
- /**
43
- * @description Input to persistentHash for computing signer commitments.
44
- * Combines the ECDSA public key with an instance-specific salt and
45
- * domain separator to produce a unique, unlinkable commitment.
46
- */
47
- export struct SignerCommitmentInput {
48
- pk: Bytes<64>,
49
- salt: Bytes<32>,
50
- domain: Bytes<32>
51
- }
52
28
 
53
29
  // ─── State ──────────────────────────────────────────────────────
54
30
 
55
31
  ledger _nonce: Counter;
56
- ledger _instanceSalt: Bytes<32>;
57
32
 
58
33
  // ─── Constructor ────────────────────────────────────────────────
59
34
 
@@ -61,14 +36,17 @@ ledger _instanceSalt: Bytes<32>;
61
36
  * @description Deploys the multisig with 3 signer commitments and
62
37
  * a threshold.
63
38
  *
64
- * Each commitment is computed off-chain as:
65
- * persistentHash(SignerCommitmentInput { pk, instanceSalt, domain })
66
- * where domain is pad(32, "MultiSig:signer:").
39
+ * Each commitment is `persistentHash(pkX, pkY, instanceSalt,
40
+ * "multisig:signer:")`. Use the exported `_calculateSignerId` circuit to
41
+ * derive them consistently with in-circuit verification.
67
42
  *
68
43
  * The instanceSalt should be a random value to prevent the same public
69
44
  * key from producing the same commitment across different multisig
70
45
  * deployments, breaking cross-contract signer correlation.
71
46
  *
47
+ * @dev A threshold of 1 does not weaken `execute`, which always verifies two
48
+ * distinct registered signers.
49
+ *
72
50
  * Requirements:
73
51
  *
74
52
  * - `thresh` must be > 0 and <= 2 (matches the 2-signature `execute` surface).
@@ -88,8 +66,7 @@ constructor(
88
66
  thresh <= 2,
89
67
  "ShieldedMultiSigV2: threshold cannot exceed 2 (execute verifies at most 2 signatures)"
90
68
  );
91
- _instanceSalt = disclose(instanceSalt);
92
- Signer_initialize<3>(signerCommitments, thresh);
69
+ EcdsaSignerManager_initialize<3>(instanceSalt, signerCommitments, thresh);
93
70
  }
94
71
 
95
72
  // ─── Deposit ────────────────────────────────────────────────────
@@ -106,6 +83,8 @@ constructor(
106
83
  * and extracting mt_index. Combined with the known ShieldedCoinInfo,
107
84
  * this produces the QualifiedShieldedCoinInfo needed for spending.
108
85
  *
86
+ * @circuitInfo k=13, rows=5898
87
+ *
109
88
  * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
110
89
  */
111
90
  export circuit deposit(coin: ShieldedCoinInfo): [] {
@@ -119,33 +98,30 @@ export circuit deposit(coin: ShieldedCoinInfo): [] {
119
98
  *
120
99
  * The circuit reads the current nonce from the ledger, increments it,
121
100
  * then reconstructs the message hash that signers must have signed
122
- * off-chain: `persistentHash(nonce, recipient address, coin color, amount)`.
101
+ * off-chain: `persistentHash([domain, kernel.self(), nonce,
102
+ * persistentHash(to), coin color, amount])`. The digest is domain-separated
103
+ * and bound to the contract instance and the full recipient (kind and
104
+ * address), so a signature authorizes exactly one operation on one instance.
123
105
  *
124
- * Signatures are verified via fold over parallel pubkey and signature
125
- * vectors. Each public key is hashed with the instance salt to produce
126
- * a commitment, checked against the signer registry, and the signature
127
- * is verified against the message hash. Duplicate signers are rejected
128
- * via inequality check on adjacent commitments.
106
+ * Approvals are checked by `EcdsaSignerManager`, which derives each signer's
107
+ * commitment from their public key and the instance salt, rejects duplicates,
108
+ * and enforces the threshold.
129
109
  *
130
- * @notice ECDSA verification is stubbed. Replace stubVerifySignature
131
- * with ecdsaVerify when Compact ECDSA primitives are available.
132
- *
133
- * @notice Duplicate detection via != only works for exactly 2 signers.
134
- * Production contracts with larger signer sets need a different
135
- * uniqueness enforcement mechanism.
110
+ * @circuitInfo k=17, rows=98061
136
111
  *
137
112
  * Requirements:
138
113
  *
139
114
  * - Both public keys must hash to registered signer commitments.
140
- * - Both signatures must be valid over the message hash.
115
+ * - Both signatures must be valid over the message hash and use the low-s
116
+ * encoding.
141
117
  * - Signers must not be duplicates.
142
118
  * - Coin value must be >= amount.
143
119
  *
144
120
  * @param {Proposal_Recipient} to - The recipient.
145
121
  * @param {Uint<128>} amount - The amount to send.
146
122
  * @param {QualifiedShieldedCoinInfo} coin - The coin to spend (from operator's pool).
147
- * @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
148
- * @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the operation.
123
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
124
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the operation.
149
125
  *
150
126
  * @returns {ShieldedSendResult} The send result including any change.
151
127
  */
@@ -153,128 +129,97 @@ export circuit execute(
153
129
  to: Proposal_Recipient,
154
130
  amount: Uint<128>,
155
131
  coin: QualifiedShieldedCoinInfo,
156
- pubkeys: Vector<2, Bytes<64>>,
157
- signatures: Vector<2, Bytes<64>>
132
+ pubkeys: Vector<2, Secp256k1Point>,
133
+ signatures: Vector<2, Secp256k1EcdsaSignature>
158
134
  ): ShieldedSendResult {
159
135
  // Increment nonce
160
136
  const currentNonce = _nonce;
161
137
  _nonce.increment(1);
162
138
 
163
139
  // Construct message hash
164
- const msgHash = persistentHash<Vector<4, Bytes<32>>>([
140
+ const msgHash = persistentHash<Vector<6, Bytes<32>>>([
141
+ pad(32, "multisig:execute:"),
142
+ kernel.self().bytes,
165
143
  currentNonce as Bytes<32>,
166
- to.address,
144
+ persistentHash<Proposal_Recipient>(to),
167
145
  coin.color,
168
146
  amount as Bytes<32>
169
147
  ]);
170
148
 
171
- // Verify signatures via fold over parallel vectors
172
- const initialState = VerificationState {
173
- validCount: 0 as Uint<8>,
174
- prevCommitment: pad(32, ""),
175
- msgHash: msgHash
176
- };
177
-
178
- const finalState = fold(verifySignature, initialState, pubkeys, signatures);
179
- Signer_assertThresholdMet(finalState.validCount);
149
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
180
150
 
181
151
  // Execute transfer
182
152
  const normalizedRecipient = Proposal_toShieldedRecipient(to);
183
153
  return Treasury__send(coin, normalizedRecipient, amount);
184
154
  }
185
155
 
186
- // ─── Signature Verification ─────────────────────────────────────
156
+ // ─── Commitments ────────────────────────────────────────────────
187
157
 
188
158
  /**
189
- * @description Fold callback. Verifies one signer's approval.
190
- *
191
- * Computes the signer's commitment from their public key and the
192
- * instance salt, checks for duplicates against the previous commitment,
193
- * verifies registry membership, and validates the ECDSA signature.
159
+ * @description Computes a signer commitment from a secp256k1 public key,
160
+ * as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
194
161
  *
195
- * @param {VerificationState} state - Accumulator threaded through fold.
196
- * @param {Bytes<64>} pubkey - The signer's ECDSA public key.
197
- * @param {Bytes<64>} signature - The signer's signature over msgHash.
198
- *
199
- * @returns {VerificationState} Updated accumulator.
200
- */
201
- circuit verifySignature(
202
- state: VerificationState,
203
- pubkey: Bytes<64>,
204
- signature: Bytes<64>
205
- ): VerificationState {
206
- const commitment = _calculateSignerId(pubkey, _instanceSalt);
207
-
208
- // Duplicate detection — sufficient for 2 signers only
209
- assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
210
-
211
- // Verify this commitment is a registered signer
212
- Signer_assertSigner(commitment);
213
-
214
- // TODO: Replace with actual ECDSA primitive when available
215
- // assert(ecdsaVerify(pubkey, state.msgHash, signature), "Multisig: invalid signature");
216
- assert(stubVerifySignature(pubkey, state.msgHash, signature), "Multisig: invalid signature");
217
-
218
- return VerificationState {
219
- validCount: state.validCount + 1 as Uint<8>,
220
- prevCommitment: commitment,
221
- msgHash: state.msgHash
222
- };
223
- }
224
-
225
- /**
226
- * @description Computes a signer commitment from an ECDSA public key.
162
+ * Pure, so the deployer can derive constructor commitments off-chain.
227
163
  *
228
- * The commitment is persistentHash(pk, salt, domain) where:
229
- * - pk: the signer's ECDSA public key (64 bytes)
230
- * - salt: instance-specific random value (prevents cross-contract correlation)
231
- * - domain: "MultiSig:signer:" (domain separation)
164
+ * Requirements:
232
165
  *
233
- * This is a pure circuit. It can be called off-chain by the deployer
234
- * to compute commitments for the constructor.
166
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
235
167
  *
236
- * @param {Bytes<64>} pk - The ECDSA public key.
168
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
237
169
  * @param {Bytes<32>} salt - The instance salt.
238
170
  *
239
171
  * @returns {Bytes<32>} The signer commitment.
240
172
  */
241
173
  export pure circuit _calculateSignerId(
242
- pk: Bytes<64>,
174
+ pk: Secp256k1Point,
243
175
  salt: Bytes<32>
244
176
  ): Bytes<32> {
245
- return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
246
- pk: pk,
247
- salt: salt,
248
- domain: pad(32, "MultiSig:signer:")
249
- });
250
- }
251
-
252
- /**
253
- * @description Stub for ECDSA signature verification.
254
- * Always returns true. MUST be replaced before any non-test deployment.
255
- */
256
- circuit stubVerifySignature(
257
- pubkey: Bytes<64>,
258
- msgHash: Bytes<32>,
259
- signature: Bytes<64>
260
- ): Boolean {
261
- return true;
177
+ return EcdsaSignerManager_calculateSignerId(pk, salt);
262
178
  }
263
179
 
264
180
  // ─── View ───────────────────────────────────────────────────────
265
181
 
182
+ /**
183
+ * @description Returns the nonce the next `execute` digest will bind to.
184
+ *
185
+ * @circuitInfo k=6, rows=34
186
+ *
187
+ * @returns {Uint<64>} The current nonce.
188
+ */
266
189
  export circuit getNonce(): Uint<64> {
267
190
  return _nonce;
268
191
  }
269
192
 
193
+ /**
194
+ * @description Returns the number of registered signers.
195
+ *
196
+ * @circuitInfo k=6, rows=34
197
+ *
198
+ * @returns {Uint<8>} The signer count.
199
+ */
270
200
  export circuit getSignerCount(): Uint<8> {
271
- return Signer_getSignerCount();
201
+ return EcdsaSignerManager_getSignerCount();
272
202
  }
273
203
 
204
+ /**
205
+ * @description Returns the approval threshold.
206
+ *
207
+ * @circuitInfo k=6, rows=34
208
+ *
209
+ * @returns {Uint<8>} The threshold.
210
+ */
274
211
  export circuit getThreshold(): Uint<8> {
275
- return Signer_getThreshold();
212
+ return EcdsaSignerManager_getThreshold();
276
213
  }
277
214
 
215
+ /**
216
+ * @description Returns whether the given commitment is a registered signer.
217
+ *
218
+ * @circuitInfo k=8, rows=129
219
+ *
220
+ * @param {Bytes<32>} commitment - The signer commitment to check.
221
+ * @returns {Boolean} True if registered.
222
+ */
278
223
  export circuit isSigner(commitment: Bytes<32>): Boolean {
279
- return Signer_isSigner(commitment);
224
+ return EcdsaSignerManager_isSigner(commitment);
280
225
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.0.1-alpha.1 (multisig/presets/ShieldedMultiSigV3.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/presets/ShieldedMultiSigV3.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @title ShieldedMultiSigV3
@@ -20,7 +20,7 @@ pragma language_version >= 0.23.0;
20
20
  *
21
21
  * - `mint` creates a new UTXO via `mintShieldedToken`, addressed to the
22
22
  * contract. No external coin input; supply is produced on-chain.
23
- * - `burn` consumes a UTXO via `sendShielded` to `burnAddress()`. Only
23
+ * - `burn` consumes a UTXO via `sendShielded` to `shieldedBurnAddress()`. Only
24
24
  * coins of this contract's token type can be burned. Change is handled
25
25
  * automatically by the transaction layer.
26
26
  *
@@ -33,47 +33,26 @@ pragma language_version >= 0.23.0;
33
33
  * hash prevent signatures for one operation type from being replayed as
34
34
  * the other.
35
35
  *
36
- * @notice ECDSA verification is stubbed. Replace `stubVerifySignature` with
37
- * `ecdsaVerify`, and `persistentHash` with `keccak256`, once the Compact
38
- * ECDSA and Keccak primitives are available.
36
+ * @notice Threshold authorization is enforced with secp256k1 ECDSA
37
+ * (`secp256k1EcdsaVerifyLowS`) over `persistentHash` digests, via
38
+ * `EcdsaSignerManager`, so a high-s signature is rejected. The approval
39
+ * vectors are fixed at `Vector<2, ...>`, so this contract is 2-of-3 only.
40
+ *
41
+ * TODO: switch every digest to `keccak256` to match the custodian's HSM
42
+ * signing format (#827).
39
43
  */
40
44
 
41
45
  import CompactStandardLibrary;
42
46
 
43
- import "../Signer"<Bytes<32>> prefix Signer_;
47
+ import "../EcdsaSignerManager" prefix EcdsaSignerManager_;
44
48
  import "../../utils/Utils" prefix Utils_;
45
49
  // For testing
46
50
  export { ZswapCoinPublicKey };
47
51
 
48
- // ─── Types ──────────────────────────────────────────────────────
49
-
50
- /**
51
- * @description Accumulator for fold-based signature verification.
52
- * Threads the valid count, previous commitment (for duplicate
53
- * detection), and message hash through each iteration.
54
- */
55
- struct VerificationState {
56
- validCount: Uint<8>,
57
- prevCommitment: Bytes<32>,
58
- msgHash: Bytes<32>
59
- }
60
-
61
- /**
62
- * @description Input to persistentHash for computing signer commitments.
63
- * Combines the ECDSA public key with an instance-specific salt and
64
- * domain separator to produce a unique, unlinkable commitment.
65
- */
66
- struct SignerCommitmentInput {
67
- pk: Bytes<64>,
68
- salt: Bytes<32>,
69
- domain: Bytes<32>
70
- }
71
-
72
52
  // ─── State ──────────────────────────────────────────────────────
73
53
 
74
54
  export ledger _counter: Counter;
75
55
  export ledger _coinNonce: Bytes<32>;
76
- export ledger _instanceSalt: Bytes<32>;
77
56
  export sealed ledger _tokenDomain: Bytes<32>;
78
57
 
79
58
  // ─── Constructor ────────────────────────────────────────────────
@@ -82,9 +61,9 @@ export sealed ledger _tokenDomain: Bytes<32>;
82
61
  * @description Deploys the contract with 3 signer commitments and
83
62
  * a threshold of 2.
84
63
  *
85
- * Each commitment is computed off-chain as:
86
- * `persistentHash(SignerCommitmentInput { pk, instanceSalt, domain })`
87
- * where domain is `pad(32, "multisig:signer:")`.
64
+ * Each commitment is `persistentHash(pkX, pkY, instanceSalt,
65
+ * "multisig:signer:")`. Use the exported `_calculateSignerId` circuit to
66
+ * derive them consistently with in-circuit verification.
88
67
  *
89
68
  * `tokenDomain` is used with `kernel.self()` to derive the token color
90
69
  * via `tokenType(_tokenDomain, kernel.self())`. Only coins of this color
@@ -109,10 +88,9 @@ constructor(
109
88
  tokenDomain: Bytes<32>,
110
89
  signerCommitments: Vector<3, Bytes<32>>,
111
90
  ) {
112
- _instanceSalt = disclose(instanceSalt);
113
91
  _coinNonce = disclose(initCoinNonce);
114
92
  _tokenDomain = disclose(tokenDomain);
115
- Signer_initialize<3>(signerCommitments, 2);
93
+ EcdsaSignerManager_initialize<3>(instanceSalt, signerCommitments, 2);
116
94
  }
117
95
 
118
96
  // ─── Mint ───────────────────────────────────────────────────────
@@ -136,9 +114,7 @@ constructor(
136
114
  * after the counter has been incremented, binding each mint's nonce to a
137
115
  * distinct counter value.
138
116
  *
139
- * @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
140
- * with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
141
- * available, to match the custodian's HSM signing format.
117
+ * @circuitInfo k=17, rows=84506
142
118
  *
143
119
  * Requirements:
144
120
  *
@@ -149,14 +125,14 @@ constructor(
149
125
  *
150
126
  * @param {Uint<64>} amount - The token amount to mint.
151
127
  * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
152
- * @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
153
- * @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the mint hash.
128
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
129
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the mint hash.
154
130
  */
155
131
  export circuit mint(
156
132
  amount: Uint<64>,
157
133
  recipient: Either<ZswapCoinPublicKey, ContractAddress>,
158
- pubkeys: Vector<2, Bytes<64>>,
159
- signatures: Vector<2, Bytes<64>>
134
+ pubkeys: Vector<2, Secp256k1Point>,
135
+ signatures: Vector<2, Secp256k1EcdsaSignature>
160
136
  ): [] {
161
137
  const opNonce = _counter;
162
138
  _counter.increment(1);
@@ -173,16 +149,7 @@ export circuit mint(
173
149
  amount as Bytes<32>
174
150
  ]);
175
151
 
176
- const initialState = VerificationState {
177
- validCount: 0 as Uint<8>,
178
- prevCommitment: pad(32, ""),
179
- msgHash: msgHash
180
- };
181
-
182
- const finalState = fold(verifySignature, initialState, pubkeys, signatures);
183
- // Thresh check not needed as it will always be 2
184
- // Leaving it as defense-in-depth
185
- Signer_assertThresholdMet(finalState.validCount);
152
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
186
153
 
187
154
  _coinNonce = evolveNonce(_counter, _coinNonce);
188
155
  mintShieldedToken(_tokenDomain, disclose(amount), _coinNonce, disclose(canonRecipient));
@@ -194,7 +161,7 @@ export circuit mint(
194
161
  * @description Burns a shielded coin of this contract's token type,
195
162
  * authorized by threshold signatures.
196
163
  *
197
- * Sends the specified amount of the supplied coin to `burnAddress()` via
164
+ * Sends the specified amount of the supplied coin to `shieldedBurnAddress()` via
198
165
  * `sendShielded`. The nullifier is submitted on-chain, permanently marking
199
166
  * the UTXO as spent.
200
167
  *
@@ -211,9 +178,7 @@ export circuit mint(
211
178
  * The "multisig:burn:" domain prefix ensures burn signatures cannot be replayed
212
179
  * as mint operations for the same parameters.
213
180
  *
214
- * @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
215
- * with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
216
- * available, to match the custodian's HSM signing format.
181
+ * @circuitInfo k=17, rows=92170
217
182
  *
218
183
  * Requirements:
219
184
  *
@@ -226,14 +191,14 @@ export circuit mint(
226
191
  *
227
192
  * @param {QualifiedShieldedCoinInfo} coin - The coin to burn (from operator's UTXO pool).
228
193
  * @param {Uint<64>} amount - The token amount to burn.
229
- * @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
230
- * @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the burn hash.
194
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
195
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn hash.
231
196
  */
232
197
  export circuit burn(
233
198
  coin: QualifiedShieldedCoinInfo,
234
199
  amount: Uint<64>,
235
- pubkeys: Vector<2, Bytes<64>>,
236
- signatures: Vector<2, Bytes<64>>
200
+ pubkeys: Vector<2, Secp256k1Point>,
201
+ signatures: Vector<2, Secp256k1EcdsaSignature>
237
202
  ): [] {
238
203
  const opNonce = _counter;
239
204
  _counter.increment(1);
@@ -245,16 +210,7 @@ export circuit burn(
245
210
  amount as Bytes<32>
246
211
  ]);
247
212
 
248
- const initialState = VerificationState {
249
- validCount: 0 as Uint<8>,
250
- prevCommitment: pad(32, ""),
251
- msgHash: msgHash
252
- };
253
-
254
- const finalState = fold(verifySignature, initialState, pubkeys, signatures);
255
- // Thresh check not needed as it will always be 2
256
- // Leaving it as defense-in-depth
257
- Signer_assertThresholdMet(finalState.validCount);
213
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
258
214
 
259
215
  assert(coin.color == tokenType(_tokenDomain, kernel.self()), "Multisig: coin not from this contract");
260
216
  assert(coin.value >= amount, "Multisig: insufficient coin value");
@@ -262,107 +218,94 @@ export circuit burn(
262
218
  sendShielded(disclose(coin), shieldedBurnAddress(), disclose(amount));
263
219
  }
264
220
 
265
- // ─── Signature Verification ─────────────────────────────────────
221
+ // ─── Commitments ────────────────────────────────────────────────
266
222
 
267
223
  /**
268
- * @description Fold callback. Verifies one signer's approval.
269
- *
270
- * Computes the signer's commitment from their public key and the instance
271
- * salt, checks for duplicates against the previous commitment, verifies
272
- * registry membership, and validates the ECDSA signature.
224
+ * @description Computes a signer commitment from a secp256k1 public key,
225
+ * as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
273
226
  *
274
- * @notice Circuit signatures are fixed at Vector<2, ...>, so this contract
275
- * supports only a fixed 2-of-3 configuration. Supporting more signatures
276
- * (e.g. 3-of-N) requires a separate variant with a larger vector and a
277
- * different duplicate-detection mechanism (sorted commitments or bitmap).
227
+ * Pure, so the deployer can derive constructor commitments off-chain.
278
228
  *
279
- * @param {VerificationState} state - Accumulator threaded through fold.
280
- * @param {Bytes<64>} pubkey - The signer's ECDSA public key.
281
- * @param {Bytes<64>} signature - The signer's signature over msgHash.
282
- * @returns {VerificationState} Updated accumulator.
283
- */
284
- circuit verifySignature(
285
- state: VerificationState,
286
- pubkey: Bytes<64>,
287
- signature: Bytes<64>
288
- ): VerificationState {
289
- const commitment = _calculateSignerId(pubkey, _instanceSalt);
290
-
291
- // Duplicate detection — sufficient for 2 signers only
292
- assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
293
-
294
- Signer_assertSigner(commitment);
295
-
296
- // TODO: Replace with ecdsaVerify + keccak256 when primitives are available
297
- assert(stubVerifySignature(pubkey, state.msgHash, signature), "Multisig: invalid signature");
298
-
299
- return VerificationState {
300
- validCount: state.validCount + 1 as Uint<8>,
301
- prevCommitment: commitment,
302
- msgHash: state.msgHash
303
- };
304
- }
305
-
306
- /**
307
- * @description Computes a signer commitment from an ECDSA public key.
308
- *
309
- * The commitment is persistentHash(pk, salt, domain) where:
310
- * - pk: the signer's ECDSA public key (64 bytes)
311
- * - salt: instance-specific random value (prevents cross-contract correlation)
312
- * - domain: "multisig:signer:" (domain separation)
229
+ * Requirements:
313
230
  *
314
- * Pure circuit — callable off-chain by the deployer to compute
315
- * commitments for the constructor.
231
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
316
232
  *
317
- * @param {Bytes<64>} pk - The ECDSA public key.
233
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
318
234
  * @param {Bytes<32>} salt - The instance salt.
319
235
  * @returns {Bytes<32>} The signer commitment.
320
236
  */
321
237
  export pure circuit _calculateSignerId(
322
- pk: Bytes<64>,
238
+ pk: Secp256k1Point,
323
239
  salt: Bytes<32>
324
240
  ): Bytes<32> {
325
- return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
326
- pk: pk,
327
- salt: salt,
328
- domain: pad(32, "multisig:signer:")
329
- });
330
- }
331
-
332
- /**
333
- * @description Stub for ECDSA signature verification.
334
- * Always returns true. MUST be replaced before any non-test deployment.
335
- */
336
- circuit stubVerifySignature(
337
- pubkey: Bytes<64>,
338
- msgHash: Bytes<32>,
339
- signature: Bytes<64>
340
- ): Boolean {
341
- return true;
241
+ return EcdsaSignerManager_calculateSignerId(pk, salt);
342
242
  }
343
243
 
344
244
  // ─── View ───────────────────────────────────────────────────────
345
245
 
246
+ /**
247
+ * @description Returns the counter the next mint or burn digest will bind to.
248
+ *
249
+ * @circuitInfo k=6, rows=34
250
+ *
251
+ * @returns {Uint<64>} The current counter.
252
+ */
346
253
  export circuit getNonce(): Uint<64> {
347
254
  return _counter;
348
255
  }
349
256
 
257
+ /**
258
+ * @description Returns the domain separator this contract's token type derives from.
259
+ *
260
+ * @circuitInfo k=6, rows=57
261
+ *
262
+ * @returns {Bytes<32>} The token domain.
263
+ */
350
264
  export circuit getTokenDomain(): Bytes<32> {
351
265
  return _tokenDomain;
352
266
  }
353
267
 
268
+ /**
269
+ * @description Returns this contract's token type.
270
+ *
271
+ * @circuitInfo k=13, rows=3836
272
+ *
273
+ * @returns {Bytes<32>} The token type.
274
+ */
354
275
  export circuit getTokenType(): Bytes<32> {
355
276
  return tokenType(_tokenDomain, kernel.self());
356
277
  }
357
278
 
279
+ /**
280
+ * @description Returns the number of registered signers.
281
+ *
282
+ * @circuitInfo k=6, rows=34
283
+ *
284
+ * @returns {Uint<8>} The signer count.
285
+ */
358
286
  export circuit getSignerCount(): Uint<8> {
359
- return Signer_getSignerCount();
287
+ return EcdsaSignerManager_getSignerCount();
360
288
  }
361
289
 
290
+ /**
291
+ * @description Returns the approval threshold.
292
+ *
293
+ * @circuitInfo k=6, rows=34
294
+ *
295
+ * @returns {Uint<8>} The threshold.
296
+ */
362
297
  export circuit getThreshold(): Uint<8> {
363
- return Signer_getThreshold();
298
+ return EcdsaSignerManager_getThreshold();
364
299
  }
365
300
 
301
+ /**
302
+ * @description Returns whether the given commitment is a registered signer.
303
+ *
304
+ * @circuitInfo k=8, rows=129
305
+ *
306
+ * @param {Bytes<32>} commitment - The signer commitment to check.
307
+ * @returns {Boolean} True if registered.
308
+ */
366
309
  export circuit isSigner(commitment: Bytes<32>): Boolean {
367
- return Signer_isSigner(commitment);
310
+ return EcdsaSignerManager_isSigner(commitment);
368
311
  }