1use alloc::collections::BTreeSet;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
6use miden_protocol::account::component::{
7 AccountComponentCode,
8 AccountComponentMetadata,
9 FeltSchema,
10 SchemaType,
11 StorageSchema,
12 StorageSlotSchema,
13};
14use miden_protocol::account::{
15 AccountComponent,
16 AccountComponentName,
17 AccountProcedureRoot,
18 StorageMap,
19 StorageMapKey,
20 StorageSlot,
21 StorageSlotName,
22};
23use miden_protocol::errors::AccountError;
24use miden_protocol::utils::sync::LazyLock;
25
26use crate::account::account_component_code;
27
28account_component_code!(MULTISIG_CODE, "auth/multisig.masl");
29
30pub(super) static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
34 StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
35 .expect("storage slot name should be valid")
36});
37
38pub(super) static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
39 StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
40 .expect("storage slot name should be valid")
41});
42
43pub(super) static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
44 StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
45 .expect("storage slot name should be valid")
46});
47
48pub(super) static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> =
49 LazyLock::new(|| {
50 StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
51 .expect("storage slot name should be valid")
52 });
53
54static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
55 StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
56 .expect("storage slot name should be valid")
57});
58
59#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct AuthMultisigConfig {
65 approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
66 default_threshold: u32,
67 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
68}
69
70impl AuthMultisigConfig {
71 pub fn new(
75 approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
76 default_threshold: u32,
77 ) -> Result<Self, AccountError> {
78 if default_threshold == 0 {
79 return Err(AccountError::other("threshold must be at least 1"));
80 }
81 if default_threshold > approvers.len() as u32 {
82 return Err(AccountError::other(
83 "threshold cannot be greater than number of approvers",
84 ));
85 }
86
87 let unique_approvers: BTreeSet<_> = approvers.iter().map(|(pk, _)| pk).collect();
89
90 if unique_approvers.len() != approvers.len() {
91 return Err(AccountError::other("duplicate approver public keys are not allowed"));
92 }
93
94 Ok(Self {
95 approvers,
96 default_threshold,
97 proc_thresholds: vec![],
98 })
99 }
100
101 pub fn with_proc_thresholds(
104 mut self,
105 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
106 ) -> Result<Self, AccountError> {
107 for (_, threshold) in &proc_thresholds {
108 if *threshold == 0 {
109 return Err(AccountError::other("procedure threshold must be at least 1"));
110 }
111 if *threshold > self.approvers.len() as u32 {
112 return Err(AccountError::other(
113 "procedure threshold cannot be greater than number of approvers",
114 ));
115 }
116 }
117 self.proc_thresholds = proc_thresholds;
118 Ok(self)
119 }
120
121 pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] {
122 &self.approvers
123 }
124
125 pub fn default_threshold(&self) -> u32 {
126 self.default_threshold
127 }
128
129 pub fn proc_thresholds(&self) -> &[(AccountProcedureRoot, u32)] {
130 &self.proc_thresholds
131 }
132}
133
134#[derive(Debug)]
142pub struct AuthMultisig {
143 config: AuthMultisigConfig,
144}
145
146impl AuthMultisig {
147 pub const NAME: &'static str = "miden::standards::components::auth::multisig";
149
150 pub const fn name() -> AccountComponentName {
152 AccountComponentName::from_static_str(Self::NAME)
153 }
154
155 pub fn code() -> &'static AccountComponentCode {
157 &MULTISIG_CODE
158 }
159
160 pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
162 Ok(Self { config })
163 }
164
165 pub fn threshold_config_slot() -> &'static StorageSlotName {
167 &THRESHOLD_CONFIG_SLOT_NAME
168 }
169
170 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
172 &APPROVER_PUBKEYS_SLOT_NAME
173 }
174
175 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
177 &APPROVER_SCHEME_ID_SLOT_NAME
178 }
179
180 pub fn executed_transactions_slot() -> &'static StorageSlotName {
182 &EXECUTED_TRANSACTIONS_SLOT_NAME
183 }
184
185 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
187 &PROCEDURE_THRESHOLDS_SLOT_NAME
188 }
189
190 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
192 (
193 Self::threshold_config_slot().clone(),
194 StorageSlotSchema::value(
195 "Threshold configuration",
196 [
197 FeltSchema::u32("threshold"),
198 FeltSchema::u32("num_approvers"),
199 FeltSchema::new_void(),
200 FeltSchema::new_void(),
201 ],
202 ),
203 )
204 }
205
206 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
208 (
209 Self::approver_public_keys_slot().clone(),
210 StorageSlotSchema::map(
211 "Approver public keys",
212 SchemaType::u32(),
213 SchemaType::pub_key(),
214 ),
215 )
216 }
217
218 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
220 (
221 Self::approver_scheme_ids_slot().clone(),
222 StorageSlotSchema::map(
223 "Approver scheme IDs",
224 SchemaType::u32(),
225 SchemaType::auth_scheme(),
226 ),
227 )
228 }
229
230 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
232 (
233 Self::executed_transactions_slot().clone(),
234 StorageSlotSchema::map(
235 "Executed transactions",
236 SchemaType::native_word(),
237 SchemaType::native_word(),
238 ),
239 )
240 }
241
242 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
244 (
245 Self::procedure_thresholds_slot().clone(),
246 StorageSlotSchema::map(
247 "Procedure thresholds",
248 SchemaType::native_word(),
249 SchemaType::u32(),
250 ),
251 )
252 }
253
254 pub fn component_metadata() -> AccountComponentMetadata {
256 let storage_schema = StorageSchema::new([
257 Self::threshold_config_slot_schema(),
258 Self::approver_public_keys_slot_schema(),
259 Self::approver_auth_scheme_slot_schema(),
260 Self::executed_transactions_slot_schema(),
261 Self::procedure_thresholds_slot_schema(),
262 ])
263 .expect("storage schema should be valid");
264
265 AccountComponentMetadata::new(Self::NAME)
266 .with_description("Multisig authentication component using hybrid signature schemes")
267 .with_storage_schema(storage_schema)
268 }
269}
270
271impl From<AuthMultisig> for AccountComponent {
272 fn from(multisig: AuthMultisig) -> Self {
273 let mut storage_slots = Vec::with_capacity(5);
274
275 let num_approvers = multisig.config.approvers().len() as u32;
277 storage_slots.push(StorageSlot::with_value(
278 AuthMultisig::threshold_config_slot().clone(),
279 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
280 ));
281
282 let map_entries =
284 multisig.config.approvers().iter().enumerate().map(|(i, (pub_key, _))| {
285 (StorageMapKey::from_index(i as u32), Word::from(*pub_key))
286 });
287
288 storage_slots.push(StorageSlot::with_map(
290 AuthMultisig::approver_public_keys_slot().clone(),
291 StorageMap::with_entries(map_entries).unwrap(),
292 ));
293
294 let scheme_id_entries =
296 multisig.config.approvers().iter().enumerate().map(|(i, (_, auth_scheme))| {
297 (StorageMapKey::from_index(i as u32), Word::from([*auth_scheme as u32, 0, 0, 0]))
298 });
299
300 storage_slots.push(StorageSlot::with_map(
301 AuthMultisig::approver_scheme_ids_slot().clone(),
302 StorageMap::with_entries(scheme_id_entries).unwrap(),
303 ));
304
305 let executed_transactions = StorageMap::default();
307 storage_slots.push(StorageSlot::with_map(
308 AuthMultisig::executed_transactions_slot().clone(),
309 executed_transactions,
310 ));
311
312 let proc_threshold_roots = StorageMap::with_entries(
314 multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
315 (StorageMapKey::from_raw(proc_root.as_word()), Word::from([*threshold, 0, 0, 0]))
316 }),
317 )
318 .unwrap();
319 storage_slots.push(StorageSlot::with_map(
320 AuthMultisig::procedure_thresholds_slot().clone(),
321 proc_threshold_roots,
322 ));
323
324 let metadata = AuthMultisig::component_metadata();
325
326 AccountComponent::new(AuthMultisig::code().clone(), storage_slots, metadata).expect(
327 "Multisig auth component should satisfy the requirements of a valid account component",
328 )
329 }
330}
331
332#[cfg(test)]
336mod tests {
337 use alloc::string::ToString;
338
339 use miden_protocol::Word;
340 use miden_protocol::account::auth::AuthSecretKey;
341 use miden_protocol::account::{AccountBuilder, auth};
342
343 use super::*;
344 use crate::account::wallets::BasicWallet;
345
346 #[test]
348 fn test_multisig_component_setup() {
349 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
351 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
352 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
353
354 let approvers = vec![
356 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
357 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
358 (sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
359 ];
360
361 let threshold = 2u32;
362
363 let multisig_component = AuthMultisig::new(
365 AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
366 )
367 .expect("multisig component creation failed");
368
369 let account = AccountBuilder::new([0; 32])
371 .with_auth_component(multisig_component)
372 .with_component(BasicWallet)
373 .build()
374 .expect("account building failed");
375
376 let config_slot = account
378 .storage()
379 .get_item(AuthMultisig::threshold_config_slot())
380 .expect("config storage slot access failed");
381 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
382
383 for (i, (expected_pub_key, _)) in approvers.iter().enumerate() {
385 let stored_pub_key = account
386 .storage()
387 .get_map_item(
388 AuthMultisig::approver_public_keys_slot(),
389 Word::from([i as u32, 0, 0, 0]),
390 )
391 .expect("approver public key storage map access failed");
392 assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
393 }
394
395 for (i, (_, expected_auth_scheme)) in approvers.iter().enumerate() {
397 let stored_scheme_id = account
398 .storage()
399 .get_map_item(
400 AuthMultisig::approver_scheme_ids_slot(),
401 Word::from([i as u32, 0, 0, 0]),
402 )
403 .expect("approver scheme ID storage map access failed");
404 assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0]));
405 }
406 }
407
408 #[test]
410 fn test_multisig_component_minimum_threshold() {
411 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
412 let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
413 let threshold = 1u32;
414
415 let multisig_component = AuthMultisig::new(
416 AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
417 )
418 .expect("multisig component creation failed");
419
420 let account = AccountBuilder::new([0; 32])
421 .with_auth_component(multisig_component)
422 .with_component(BasicWallet)
423 .build()
424 .expect("account building failed");
425
426 let config_slot = account
428 .storage()
429 .get_item(AuthMultisig::threshold_config_slot())
430 .expect("config storage slot access failed");
431 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
432
433 let stored_pub_key = account
434 .storage()
435 .get_map_item(AuthMultisig::approver_public_keys_slot(), Word::from([0u32, 0, 0, 0]))
436 .expect("approver pub keys storage map access failed");
437 assert_eq!(stored_pub_key, Word::from(pub_key));
438
439 let stored_scheme_id = account
440 .storage()
441 .get_map_item(AuthMultisig::approver_scheme_ids_slot(), Word::from([0u32, 0, 0, 0]))
442 .expect("approver scheme IDs storage map access failed");
443 assert_eq!(
444 stored_scheme_id,
445 Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
446 );
447 }
448
449 #[test]
451 fn test_multisig_component_error_cases() {
452 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
453 let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
454
455 let result = AuthMultisigConfig::new(approvers.clone(), 0);
457 assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
458
459 let result = AuthMultisigConfig::new(approvers, 2);
461 assert!(
462 result
463 .unwrap_err()
464 .to_string()
465 .contains("threshold cannot be greater than number of approvers")
466 );
467 }
468
469 #[test]
471 fn test_multisig_component_duplicate_approvers() {
472 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
474 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
475
476 let approvers = vec![
478 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
479 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
480 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
481 ];
482
483 let result = AuthMultisigConfig::new(approvers, 2);
484 assert!(
485 result
486 .unwrap_err()
487 .to_string()
488 .contains("duplicate approver public keys are not allowed")
489 );
490 }
491}