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