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