miden_standards/account/auth/
multisig.rs1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::component::{
5 AccountComponentCode,
6 AccountComponentMetadata,
7 FeltSchema,
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::{Approver, ApproverSet};
25use crate::account::account_component_code;
26
27account_component_code!(MULTISIG_CODE, "miden-standards-auth-multisig.masp");
28
29pub(super) static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
33 StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
34 .expect("storage slot name should be valid")
35});
36
37pub(super) static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
38 StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
39 .expect("storage slot name should be valid")
40});
41
42pub(super) static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
43 StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
44 .expect("storage slot name should be valid")
45});
46
47pub(super) static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> =
48 LazyLock::new(|| {
49 StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
50 .expect("storage slot name should be valid")
51 });
52
53static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
54 StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
55 .expect("storage slot name should be valid")
56});
57
58#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct AuthMultisigConfig {
64 approver_set: ApproverSet,
65 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
66}
67
68impl AuthMultisigConfig {
69 pub fn new(approver_set: ApproverSet) -> Self {
71 Self {
72 approver_set,
73 proc_thresholds: Vec::new(),
74 }
75 }
76
77 pub fn with_proc_thresholds(
80 mut self,
81 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
82 ) -> Result<Self, AccountError> {
83 let num_approvers = self.approver_set.approvers().len() as u32;
84 for (_, threshold) in &proc_thresholds {
85 if *threshold == 0 {
86 return Err(AccountError::other("procedure threshold must be at least 1"));
87 }
88 if *threshold > num_approvers {
89 return Err(AccountError::other(
90 "procedure threshold cannot be greater than number of approvers",
91 ));
92 }
93 }
94 self.proc_thresholds = proc_thresholds;
95 Ok(self)
96 }
97
98 pub fn approver_set(&self) -> &ApproverSet {
99 &self.approver_set
100 }
101
102 pub fn approvers(&self) -> &[Approver] {
103 self.approver_set.approvers()
104 }
105
106 pub fn default_threshold(&self) -> u32 {
107 self.approver_set.threshold().get()
108 }
109
110 pub fn proc_thresholds(&self) -> &[(AccountProcedureRoot, u32)] {
111 &self.proc_thresholds
112 }
113}
114
115#[derive(Debug)]
161pub struct AuthMultisig {
162 config: AuthMultisigConfig,
163}
164
165impl AuthMultisig {
166 pub const NAME: &'static str = "miden::standards::components::auth::multisig";
168
169 pub const fn name() -> AccountComponentName {
171 AccountComponentName::from_static_str(Self::NAME)
172 }
173
174 pub fn code() -> &'static AccountComponentCode {
176 &MULTISIG_CODE
177 }
178
179 pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
181 Ok(Self { config })
182 }
183
184 pub fn threshold_config_slot() -> &'static StorageSlotName {
186 &THRESHOLD_CONFIG_SLOT_NAME
187 }
188
189 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
191 &APPROVER_PUBKEYS_SLOT_NAME
192 }
193
194 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
196 &APPROVER_SCHEME_ID_SLOT_NAME
197 }
198
199 pub fn executed_transactions_slot() -> &'static StorageSlotName {
201 &EXECUTED_TRANSACTIONS_SLOT_NAME
202 }
203
204 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
206 &PROCEDURE_THRESHOLDS_SLOT_NAME
207 }
208
209 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
211 (
212 Self::threshold_config_slot().clone(),
213 StorageSlotSchema::value(
214 "Threshold configuration",
215 [
216 FeltSchema::u32("threshold"),
217 FeltSchema::u32("num_approvers"),
218 FeltSchema::new_void(),
219 FeltSchema::new_void(),
220 ],
221 ),
222 )
223 }
224
225 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
227 (
228 Self::approver_public_keys_slot().clone(),
229 StorageSlotSchema::map(
230 "Approver public keys",
231 SchemaType::u32(),
232 SchemaType::pub_key(),
233 ),
234 )
235 }
236
237 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
239 (
240 Self::approver_scheme_ids_slot().clone(),
241 StorageSlotSchema::map(
242 "Approver scheme IDs",
243 SchemaType::u32(),
244 SchemaType::auth_scheme(),
245 ),
246 )
247 }
248
249 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
251 (
252 Self::executed_transactions_slot().clone(),
253 StorageSlotSchema::map(
254 "Executed transactions",
255 SchemaType::native_word(),
256 SchemaType::native_word(),
257 ),
258 )
259 }
260
261 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
263 (
264 Self::procedure_thresholds_slot().clone(),
265 StorageSlotSchema::map(
266 "Procedure thresholds",
267 SchemaType::native_word(),
268 SchemaType::u32(),
269 ),
270 )
271 }
272
273 pub fn component_metadata() -> AccountComponentMetadata {
275 let storage_schema = StorageSchema::new([
276 Self::threshold_config_slot_schema(),
277 Self::approver_public_keys_slot_schema(),
278 Self::approver_auth_scheme_slot_schema(),
279 Self::executed_transactions_slot_schema(),
280 Self::procedure_thresholds_slot_schema(),
281 ])
282 .expect("storage schema should be valid");
283
284 AccountComponentMetadata::new(Self::NAME)
285 .with_description("Multisig authentication component using hybrid signature schemes")
286 .with_storage_schema(storage_schema)
287 }
288}
289
290impl From<AuthMultisig> for AccountComponent {
291 fn from(multisig: AuthMultisig) -> Self {
292 let mut storage_slots = Vec::with_capacity(5);
293
294 let num_approvers = multisig.config.approvers().len() as u32;
296 storage_slots.push(StorageSlot::with_value(
297 AuthMultisig::threshold_config_slot().clone(),
298 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
299 ));
300
301 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
303 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
304 });
305
306 storage_slots.push(StorageSlot::with_map(
308 AuthMultisig::approver_public_keys_slot().clone(),
309 StorageMap::with_entries(map_entries).unwrap(),
310 ));
311
312 let scheme_id_entries =
314 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
315 (
316 StorageMapKey::from_index(i as u32),
317 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
318 )
319 });
320
321 storage_slots.push(StorageSlot::with_map(
322 AuthMultisig::approver_scheme_ids_slot().clone(),
323 StorageMap::with_entries(scheme_id_entries).unwrap(),
324 ));
325
326 let executed_transactions = StorageMap::default();
328 storage_slots.push(StorageSlot::with_map(
329 AuthMultisig::executed_transactions_slot().clone(),
330 executed_transactions,
331 ));
332
333 let proc_threshold_roots = StorageMap::with_entries(
335 multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
336 (StorageMapKey::from_raw(proc_root.as_word()), Word::from([*threshold, 0, 0, 0]))
337 }),
338 )
339 .unwrap();
340 storage_slots.push(StorageSlot::with_map(
341 AuthMultisig::procedure_thresholds_slot().clone(),
342 proc_threshold_roots,
343 ));
344
345 let metadata = AuthMultisig::component_metadata();
346
347 AccountComponent::new(AuthMultisig::code().clone(), storage_slots, metadata).expect(
348 "Multisig auth component should satisfy the requirements of a valid account component",
349 )
350 }
351}
352
353#[cfg(test)]
357mod tests {
358 use alloc::string::ToString;
359
360 use miden_protocol::Word;
361 use miden_protocol::account::auth::AuthSecretKey;
362 use miden_protocol::account::{AccountBuilder, auth};
363
364 use super::*;
365 use crate::account::wallets::BasicWallet;
366
367 #[test]
369 fn test_multisig_component_setup() {
370 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
372 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
373 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
374
375 let approvers = vec![
377 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
378 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
379 Approver::new(sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
380 ];
381
382 let threshold = 2u32;
383
384 let approver_set =
386 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
387 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
388 .expect("multisig component creation failed");
389
390 let account = AccountBuilder::new([0; 32])
392 .with_auth_component(multisig_component)
393 .with_component(BasicWallet)
394 .build()
395 .expect("account building failed");
396
397 let config_slot = account
399 .storage()
400 .get_item(AuthMultisig::threshold_config_slot())
401 .expect("config storage slot access failed");
402 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
403
404 for (i, approver) in approvers.iter().enumerate() {
406 let stored_pub_key = account
407 .storage()
408 .get_map_item(
409 AuthMultisig::approver_public_keys_slot(),
410 StorageMapKey::from_index(i as u32),
411 )
412 .expect("approver public key storage map access failed");
413 assert_eq!(stored_pub_key, Word::from(approver.pub_key()));
414 }
415
416 for (i, approver) in approvers.iter().enumerate() {
418 let stored_scheme_id = account
419 .storage()
420 .get_map_item(
421 AuthMultisig::approver_scheme_ids_slot(),
422 StorageMapKey::from_index(i as u32),
423 )
424 .expect("approver scheme ID storage map access failed");
425 assert_eq!(stored_scheme_id, Word::from([approver.auth_scheme() as u32, 0, 0, 0]));
426 }
427 }
428
429 #[test]
431 fn test_multisig_component_minimum_threshold() {
432 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
433 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
434 let threshold = 1u32;
435
436 let approver_set =
437 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
438 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
439 .expect("multisig component creation failed");
440
441 let account = AccountBuilder::new([0; 32])
442 .with_auth_component(multisig_component)
443 .with_component(BasicWallet)
444 .build()
445 .expect("account building failed");
446
447 let config_slot = account
449 .storage()
450 .get_item(AuthMultisig::threshold_config_slot())
451 .expect("config storage slot access failed");
452 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
453
454 let stored_pub_key = account
455 .storage()
456 .get_map_item(AuthMultisig::approver_public_keys_slot(), StorageMapKey::from_index(0))
457 .expect("approver pub keys storage map access failed");
458 assert_eq!(stored_pub_key, Word::from(pub_key));
459
460 let stored_scheme_id = account
461 .storage()
462 .get_map_item(AuthMultisig::approver_scheme_ids_slot(), StorageMapKey::from_index(0))
463 .expect("approver scheme IDs storage map access failed");
464 assert_eq!(
465 stored_scheme_id,
466 Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
467 );
468 }
469
470 #[test]
472 fn test_proc_threshold_too_high() {
473 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
474 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
475 let approver_set = ApproverSet::new(approvers, 1).expect("invalid approver set");
476
477 let result = AuthMultisigConfig::new(approver_set)
478 .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 2)]);
479 assert!(
480 result
481 .unwrap_err()
482 .to_string()
483 .contains("procedure threshold cannot be greater than number of approvers")
484 );
485 }
486}