miden_standards/account/auth/
guarded_multisig.rs1use alloc::collections::BTreeMap;
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 SchemaType,
10 StorageSchema,
11 StorageSlotSchema,
12};
13use miden_protocol::account::{
14 AccountComponent,
15 AccountComponentName,
16 AccountProcedureRoot,
17 StorageMap,
18 StorageMapKey,
19 StorageSlot,
20 StorageSlotName,
21};
22use miden_protocol::errors::AccountError;
23use miden_protocol::utils::sync::LazyLock;
24
25use super::multisig::{AuthMultisig, AuthMultisigConfig};
26use super::{Approver, ApproverSet};
27use crate::account::account_component_code;
28
29account_component_code!(GUARDED_MULTISIG_CODE, "miden-standards-auth-guarded-multisig.masp");
30
31static GUARDIAN_PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
35 StorageSlotName::new("miden::standards::auth::guardian::pub_key")
36 .expect("storage slot name should be valid")
37});
38
39static GUARDIAN_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
40 StorageSlotName::new("miden::standards::auth::guardian::scheme")
41 .expect("storage slot name should be valid")
42});
43
44#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct AuthGuardedMultisigConfig {
50 multisig: AuthMultisigConfig,
51 guardian_config: GuardianConfig,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct GuardianConfig {
57 approver: Approver,
58}
59
60impl GuardianConfig {
61 pub fn new(approver: Approver) -> Self {
62 Self { approver }
63 }
64
65 pub fn approver(&self) -> Approver {
66 self.approver
67 }
68
69 pub fn pub_key(&self) -> PublicKeyCommitment {
70 self.approver.pub_key()
71 }
72
73 pub fn auth_scheme(&self) -> AuthScheme {
74 self.approver.auth_scheme()
75 }
76
77 fn public_key_slot() -> &'static StorageSlotName {
78 &GUARDIAN_PUBKEY_SLOT_NAME
79 }
80
81 fn scheme_id_slot() -> &'static StorageSlotName {
82 &GUARDIAN_SCHEME_ID_SLOT_NAME
83 }
84
85 fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
86 (
87 Self::public_key_slot().clone(),
88 StorageSlotSchema::map(
89 "Guardian public keys",
90 SchemaType::u32(),
91 SchemaType::pub_key(),
92 ),
93 )
94 }
95
96 fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
97 (
98 Self::scheme_id_slot().clone(),
99 StorageSlotSchema::map(
100 "Guardian scheme IDs",
101 SchemaType::u32(),
102 SchemaType::auth_scheme(),
103 ),
104 )
105 }
106
107 fn into_component_parts(self) -> (Vec<StorageSlot>, Vec<(StorageSlotName, StorageSlotSchema)>) {
108 let mut storage_slots = Vec::with_capacity(2);
109
110 let guardian_public_key_entries =
112 [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key()))];
113 storage_slots.push(StorageSlot::with_map(
114 Self::public_key_slot().clone(),
115 StorageMap::with_entries(guardian_public_key_entries).unwrap(),
116 ));
117
118 let guardian_scheme_id_entries = [(
120 StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])),
121 Word::from([self.auth_scheme() as u32, 0, 0, 0]),
122 )];
123 storage_slots.push(StorageSlot::with_map(
124 Self::scheme_id_slot().clone(),
125 StorageMap::with_entries(guardian_scheme_id_entries).unwrap(),
126 ));
127
128 let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()];
129
130 (storage_slots, slot_metadata)
131 }
132}
133
134impl AuthGuardedMultisigConfig {
135 pub fn new(
139 approver_set: ApproverSet,
140 guardian_config: GuardianConfig,
141 ) -> Result<Self, AccountError> {
142 if approver_set
143 .approvers()
144 .iter()
145 .any(|approver| approver.pub_key() == guardian_config.pub_key())
146 {
147 return Err(AccountError::other(
148 "guardian public key must be different from approvers",
149 ));
150 }
151
152 Ok(Self {
153 multisig: AuthMultisigConfig::new(approver_set),
154 guardian_config,
155 })
156 }
157
158 pub fn with_proc_thresholds(
161 mut self,
162 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
163 ) -> Result<Self, AccountError> {
164 self.multisig = self.multisig.with_proc_thresholds(proc_thresholds)?;
165 Ok(self)
166 }
167
168 pub fn approver_set(&self) -> &ApproverSet {
169 self.multisig.approver_set()
170 }
171
172 pub fn approvers(&self) -> &[Approver] {
173 self.multisig.approvers()
174 }
175
176 pub fn default_threshold(&self) -> u32 {
177 self.multisig.default_threshold()
178 }
179
180 pub fn proc_thresholds(&self) -> &BTreeMap<AccountProcedureRoot, u32> {
181 self.multisig.proc_thresholds()
182 }
183
184 pub fn guardian_config(&self) -> GuardianConfig {
185 self.guardian_config
186 }
187
188 fn into_parts(self) -> (AuthMultisigConfig, GuardianConfig) {
189 (self.multisig, self.guardian_config)
190 }
191}
192
193#[derive(Debug)]
226pub struct AuthGuardedMultisig {
227 multisig: AuthMultisig,
228 guardian_config: GuardianConfig,
229}
230
231impl AuthGuardedMultisig {
232 pub const NAME: &'static str = "miden::standards::auth::guarded_multisig";
234
235 pub const fn name() -> AccountComponentName {
237 AccountComponentName::from_static_str(Self::NAME)
238 }
239
240 pub fn code() -> &'static AccountComponentCode {
242 &GUARDED_MULTISIG_CODE
243 }
244
245 pub fn new(config: AuthGuardedMultisigConfig) -> Result<Self, AccountError> {
247 let (multisig_config, guardian_config) = config.into_parts();
248 Ok(Self {
249 multisig: AuthMultisig::new(multisig_config)?,
250 guardian_config,
251 })
252 }
253
254 pub fn threshold_config_slot() -> &'static StorageSlotName {
256 AuthMultisig::threshold_config_slot()
257 }
258
259 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
261 AuthMultisig::approver_public_keys_slot()
262 }
263
264 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
266 AuthMultisig::approver_scheme_ids_slot()
267 }
268
269 pub fn executed_transactions_slot() -> &'static StorageSlotName {
271 AuthMultisig::executed_transactions_slot()
272 }
273
274 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
276 AuthMultisig::procedure_thresholds_slot()
277 }
278
279 pub fn guardian_public_key_slot() -> &'static StorageSlotName {
281 GuardianConfig::public_key_slot()
282 }
283
284 pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
286 GuardianConfig::scheme_id_slot()
287 }
288
289 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
291 AuthMultisig::threshold_config_slot_schema()
292 }
293
294 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
296 AuthMultisig::approver_public_keys_slot_schema()
297 }
298
299 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
301 AuthMultisig::approver_auth_scheme_slot_schema()
302 }
303
304 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
306 AuthMultisig::executed_transactions_slot_schema()
307 }
308
309 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
311 AuthMultisig::procedure_thresholds_slot_schema()
312 }
313
314 pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
316 GuardianConfig::public_key_slot_schema()
317 }
318
319 pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
321 GuardianConfig::auth_scheme_slot_schema()
322 }
323
324 pub fn component_metadata() -> AccountComponentMetadata {
326 let storage_schema = StorageSchema::new([
327 Self::threshold_config_slot_schema(),
328 Self::approver_public_keys_slot_schema(),
329 Self::approver_auth_scheme_slot_schema(),
330 Self::executed_transactions_slot_schema(),
331 Self::procedure_thresholds_slot_schema(),
332 Self::guardian_public_key_slot_schema(),
333 Self::guardian_auth_scheme_slot_schema(),
334 ])
335 .expect("storage schema should be valid");
336
337 AccountComponentMetadata::new(Self::NAME)
338 .with_description(
339 "Guarded multisig authentication component integrated \
340 with a state guardian using hybrid signature schemes",
341 )
342 .with_storage_schema(storage_schema)
343 }
344}
345
346impl From<AuthGuardedMultisig> for AccountComponent {
347 fn from(multisig: AuthGuardedMultisig) -> Self {
348 let AuthGuardedMultisig { multisig, guardian_config } = multisig;
349 let multisig_component = AccountComponent::from(multisig);
350 let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts();
351
352 let mut storage_slots = multisig_component.storage_slots().to_vec();
353 storage_slots.extend(guardian_slots);
354
355 let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component
356 .storage_schema()
357 .iter()
358 .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone()))
359 .collect();
360 slot_schemas.extend(guardian_slot_metadata);
361
362 let storage_schema =
363 StorageSchema::new(slot_schemas).expect("storage schema should be valid");
364
365 let metadata = AccountComponentMetadata::new(AuthGuardedMultisig::NAME)
366 .with_description(multisig_component.metadata().description())
367 .with_version(multisig_component.metadata().version().clone())
368 .with_storage_schema(storage_schema);
369
370 AccountComponent::new(AuthGuardedMultisig::code().clone(), storage_slots, metadata).expect(
371 "Guarded multisig auth component should satisfy the requirements of a valid \
372 account component",
373 )
374 }
375}
376
377#[cfg(test)]
381mod tests {
382 use alloc::string::ToString;
383
384 use miden_protocol::Word;
385 use miden_protocol::account::AccountBuilder;
386 use miden_protocol::account::auth::AuthSecretKey;
387
388 use super::*;
389 use crate::account::wallets::BasicWallet;
390
391 fn approver(key: &AuthSecretKey) -> Approver {
392 Approver::new(key.public_key().to_commitment(), key.auth_scheme())
393 }
394
395 #[test]
397 fn test_guarded_multisig_component_setup() {
398 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
400 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
401 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
402 let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
403
404 let approvers = vec![approver(&sec_key_1), approver(&sec_key_2), approver(&sec_key_3)];
406
407 let threshold = 2u32;
408
409 let approver_set =
411 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
412 let multisig_component = AuthGuardedMultisig::new(
413 AuthGuardedMultisigConfig::new(
414 approver_set,
415 GuardianConfig::new(approver(&guardian_key)),
416 )
417 .expect("invalid guarded multisig config"),
418 )
419 .expect("guarded multisig component creation failed");
420
421 let account = AccountBuilder::new([0; 32])
423 .with_component(multisig_component)
424 .with_component(BasicWallet)
425 .build()
426 .expect("account building failed");
427
428 let config_slot = account
430 .storage()
431 .get_item(AuthGuardedMultisig::threshold_config_slot())
432 .expect("config storage slot access failed");
433 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
434
435 for (i, expected) in approvers.iter().enumerate() {
437 let stored_pub_key = account
438 .storage()
439 .get_map_item(
440 AuthGuardedMultisig::approver_public_keys_slot(),
441 StorageMapKey::from_index(i as u32),
442 )
443 .expect("approver public key storage map access failed");
444 assert_eq!(stored_pub_key, Word::from(expected.pub_key()));
445 }
446
447 for (i, expected) in approvers.iter().enumerate() {
449 let stored_scheme_id = account
450 .storage()
451 .get_map_item(
452 AuthGuardedMultisig::approver_scheme_ids_slot(),
453 StorageMapKey::from_index(i as u32),
454 )
455 .expect("approver scheme ID storage map access failed");
456 assert_eq!(stored_scheme_id, Word::from([expected.auth_scheme() as u32, 0, 0, 0]));
457 }
458
459 let guardian_public_key = account
461 .storage()
462 .get_map_item(
463 AuthGuardedMultisig::guardian_public_key_slot(),
464 StorageMapKey::from_index(0),
465 )
466 .expect("guardian public key storage map access failed");
467 assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
468
469 let guardian_scheme_id = account
470 .storage()
471 .get_map_item(
472 AuthGuardedMultisig::guardian_scheme_id_slot(),
473 StorageMapKey::from_index(0),
474 )
475 .expect("guardian scheme ID storage map access failed");
476 assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
477 }
478
479 #[test]
481 fn test_guarded_multisig_component_minimum_threshold() {
482 let approver_key = AuthSecretKey::new_ecdsa_k256_keccak();
483 let pub_key = approver_key.public_key().to_commitment();
484 let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
485 let approvers = vec![approver(&approver_key)];
486 let threshold = 1u32;
487
488 let approver_set =
489 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
490 let multisig_component = AuthGuardedMultisig::new(
491 AuthGuardedMultisigConfig::new(
492 approver_set,
493 GuardianConfig::new(approver(&guardian_key)),
494 )
495 .expect("invalid guarded multisig config"),
496 )
497 .expect("guarded multisig component creation failed");
498
499 let account = AccountBuilder::new([0; 32])
500 .with_component(multisig_component)
501 .with_component(BasicWallet)
502 .build()
503 .expect("account building failed");
504
505 let config_slot = account
507 .storage()
508 .get_item(AuthGuardedMultisig::threshold_config_slot())
509 .expect("config storage slot access failed");
510 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
511
512 let stored_pub_key = account
513 .storage()
514 .get_map_item(
515 AuthGuardedMultisig::approver_public_keys_slot(),
516 StorageMapKey::from_index(0),
517 )
518 .expect("approver pub keys storage map access failed");
519 assert_eq!(stored_pub_key, Word::from(pub_key));
520
521 let stored_scheme_id = account
522 .storage()
523 .get_map_item(
524 AuthGuardedMultisig::approver_scheme_ids_slot(),
525 StorageMapKey::from_index(0),
526 )
527 .expect("approver scheme IDs storage map access failed");
528 assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0]));
529 }
530
531 #[test]
533 fn test_guarded_multisig_component_guardian_not_approver() {
534 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
535 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
536
537 let approvers = vec![approver(&sec_key_1), approver(&sec_key_2)];
538 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
539
540 let result =
541 AuthGuardedMultisigConfig::new(approver_set, GuardianConfig::new(approver(&sec_key_1)));
542
543 assert!(
544 result
545 .unwrap_err()
546 .to_string()
547 .contains("guardian public key must be different from approvers")
548 );
549 }
550}