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)]
237pub struct AuthGuardedMultisig {
238 multisig: AuthMultisig,
239 guardian_config: GuardianConfig,
240}
241
242impl AuthGuardedMultisig {
243 pub const NAME: &'static str = "miden::standards::auth::guarded_multisig";
245
246 pub const fn name() -> AccountComponentName {
248 AccountComponentName::from_static_str(Self::NAME)
249 }
250
251 pub fn code() -> &'static AccountComponentCode {
253 &GUARDED_MULTISIG_CODE
254 }
255
256 pub fn new(config: AuthGuardedMultisigConfig) -> Result<Self, AccountError> {
258 let (multisig_config, guardian_config) = config.into_parts();
259 Ok(Self {
260 multisig: AuthMultisig::new(multisig_config)?,
261 guardian_config,
262 })
263 }
264
265 pub fn threshold_config_slot() -> &'static StorageSlotName {
267 AuthMultisig::threshold_config_slot()
268 }
269
270 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
272 AuthMultisig::approver_public_keys_slot()
273 }
274
275 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
277 AuthMultisig::approver_scheme_ids_slot()
278 }
279
280 pub fn executed_transactions_slot() -> &'static StorageSlotName {
282 AuthMultisig::executed_transactions_slot()
283 }
284
285 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
287 AuthMultisig::procedure_thresholds_slot()
288 }
289
290 pub fn guardian_public_key_slot() -> &'static StorageSlotName {
292 GuardianConfig::public_key_slot()
293 }
294
295 pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
297 GuardianConfig::scheme_id_slot()
298 }
299
300 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
302 AuthMultisig::threshold_config_slot_schema()
303 }
304
305 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
307 AuthMultisig::approver_public_keys_slot_schema()
308 }
309
310 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
312 AuthMultisig::approver_auth_scheme_slot_schema()
313 }
314
315 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
317 AuthMultisig::executed_transactions_slot_schema()
318 }
319
320 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
322 AuthMultisig::procedure_thresholds_slot_schema()
323 }
324
325 pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
327 GuardianConfig::public_key_slot_schema()
328 }
329
330 pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
332 GuardianConfig::auth_scheme_slot_schema()
333 }
334
335 pub fn component_metadata() -> AccountComponentMetadata {
337 let storage_schema = StorageSchema::new([
338 Self::threshold_config_slot_schema(),
339 Self::approver_public_keys_slot_schema(),
340 Self::approver_auth_scheme_slot_schema(),
341 Self::executed_transactions_slot_schema(),
342 Self::procedure_thresholds_slot_schema(),
343 Self::guardian_public_key_slot_schema(),
344 Self::guardian_auth_scheme_slot_schema(),
345 ])
346 .expect("storage schema should be valid");
347
348 AccountComponentMetadata::new(Self::NAME)
349 .with_description(
350 "Guarded multisig authentication component integrated \
351 with a state guardian using hybrid signature schemes",
352 )
353 .with_storage_schema(storage_schema)
354 }
355}
356
357impl From<AuthGuardedMultisig> for AccountComponent {
358 fn from(multisig: AuthGuardedMultisig) -> Self {
359 let AuthGuardedMultisig { multisig, guardian_config } = multisig;
360 let multisig_component = AccountComponent::from(multisig);
361 let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts();
362
363 let mut storage_slots = multisig_component.storage_slots().to_vec();
364 storage_slots.extend(guardian_slots);
365
366 let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component
367 .storage_schema()
368 .iter()
369 .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone()))
370 .collect();
371 slot_schemas.extend(guardian_slot_metadata);
372
373 let storage_schema =
374 StorageSchema::new(slot_schemas).expect("storage schema should be valid");
375
376 let metadata = AccountComponentMetadata::new(AuthGuardedMultisig::NAME)
377 .with_description(multisig_component.metadata().description())
378 .with_version(multisig_component.metadata().version().clone())
379 .with_storage_schema(storage_schema);
380
381 AccountComponent::new(AuthGuardedMultisig::code().clone(), storage_slots, metadata).expect(
382 "Guarded multisig auth component should satisfy the requirements of a valid \
383 account component",
384 )
385 }
386}
387
388#[cfg(test)]
392mod tests {
393 use alloc::string::ToString;
394
395 use miden_protocol::Word;
396 use miden_protocol::account::AccountBuilder;
397 use miden_protocol::account::auth::AuthSecretKey;
398
399 use super::*;
400 use crate::account::wallets::BasicWallet;
401
402 fn approver(key: &AuthSecretKey) -> Approver {
403 Approver::new(key.public_key().to_commitment(), key.auth_scheme())
404 }
405
406 #[test]
408 fn test_guarded_multisig_component_setup() {
409 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
411 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
412 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
413 let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
414
415 let approvers = vec![approver(&sec_key_1), approver(&sec_key_2), approver(&sec_key_3)];
417
418 let threshold = 2u32;
419
420 let approver_set =
422 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
423 let multisig_component = AuthGuardedMultisig::new(
424 AuthGuardedMultisigConfig::new(
425 approver_set,
426 GuardianConfig::new(approver(&guardian_key)),
427 )
428 .expect("invalid guarded multisig config"),
429 )
430 .expect("guarded multisig component creation failed");
431
432 let account = AccountBuilder::new([0; 32])
434 .with_component(multisig_component)
435 .with_component(BasicWallet)
436 .build()
437 .expect("account building failed");
438
439 let config_slot = account
441 .storage()
442 .get_item(AuthGuardedMultisig::threshold_config_slot())
443 .expect("config storage slot access failed");
444 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
445
446 for (i, expected) in approvers.iter().enumerate() {
448 let stored_pub_key = account
449 .storage()
450 .get_map_item(
451 AuthGuardedMultisig::approver_public_keys_slot(),
452 StorageMapKey::from_index(i as u32),
453 )
454 .expect("approver public key storage map access failed");
455 assert_eq!(stored_pub_key, Word::from(expected.pub_key()));
456 }
457
458 for (i, expected) in approvers.iter().enumerate() {
460 let stored_scheme_id = account
461 .storage()
462 .get_map_item(
463 AuthGuardedMultisig::approver_scheme_ids_slot(),
464 StorageMapKey::from_index(i as u32),
465 )
466 .expect("approver scheme ID storage map access failed");
467 assert_eq!(stored_scheme_id, Word::from([expected.auth_scheme() as u32, 0, 0, 0]));
468 }
469
470 let guardian_public_key = account
472 .storage()
473 .get_map_item(
474 AuthGuardedMultisig::guardian_public_key_slot(),
475 StorageMapKey::from_index(0),
476 )
477 .expect("guardian public key storage map access failed");
478 assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
479
480 let guardian_scheme_id = account
481 .storage()
482 .get_map_item(
483 AuthGuardedMultisig::guardian_scheme_id_slot(),
484 StorageMapKey::from_index(0),
485 )
486 .expect("guardian scheme ID storage map access failed");
487 assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
488 }
489
490 #[test]
492 fn test_guarded_multisig_component_minimum_threshold() {
493 let approver_key = AuthSecretKey::new_ecdsa_k256_keccak();
494 let pub_key = approver_key.public_key().to_commitment();
495 let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
496 let approvers = vec![approver(&approver_key)];
497 let threshold = 1u32;
498
499 let approver_set =
500 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
501 let multisig_component = AuthGuardedMultisig::new(
502 AuthGuardedMultisigConfig::new(
503 approver_set,
504 GuardianConfig::new(approver(&guardian_key)),
505 )
506 .expect("invalid guarded multisig config"),
507 )
508 .expect("guarded multisig component creation failed");
509
510 let account = AccountBuilder::new([0; 32])
511 .with_component(multisig_component)
512 .with_component(BasicWallet)
513 .build()
514 .expect("account building failed");
515
516 let config_slot = account
518 .storage()
519 .get_item(AuthGuardedMultisig::threshold_config_slot())
520 .expect("config storage slot access failed");
521 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
522
523 let stored_pub_key = account
524 .storage()
525 .get_map_item(
526 AuthGuardedMultisig::approver_public_keys_slot(),
527 StorageMapKey::from_index(0),
528 )
529 .expect("approver pub keys storage map access failed");
530 assert_eq!(stored_pub_key, Word::from(pub_key));
531
532 let stored_scheme_id = account
533 .storage()
534 .get_map_item(
535 AuthGuardedMultisig::approver_scheme_ids_slot(),
536 StorageMapKey::from_index(0),
537 )
538 .expect("approver scheme IDs storage map access failed");
539 assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0]));
540 }
541
542 #[test]
544 fn test_guarded_multisig_component_guardian_not_approver() {
545 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
546 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
547
548 let approvers = vec![approver(&sec_key_1), approver(&sec_key_2)];
549 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
550
551 let result =
552 AuthGuardedMultisigConfig::new(approver_set, GuardianConfig::new(approver(&sec_key_1)));
553
554 assert!(
555 result
556 .unwrap_err()
557 .to_string()
558 .contains("guardian public key must be different from approvers")
559 );
560 }
561}