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