miden_standards/account/auth/multisig_smart/
component.rs1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::component::{
5 AccountComponentCode,
6 AccountComponentMetadata,
7 SchemaType,
8 StorageSchema,
9 StorageSlotSchema,
10};
11use miden_protocol::account::{
12 AccountComponent,
13 AccountProcedureRoot,
14 StorageMap,
15 StorageMapKey,
16 StorageSlot,
17 StorageSlotName,
18};
19use miden_protocol::errors::AccountError;
20use miden_protocol::utils::sync::LazyLock;
21
22use super::super::multisig::{
26 APPROVER_PUBKEYS_SLOT_NAME,
27 APPROVER_SCHEME_ID_SLOT_NAME,
28 EXECUTED_TRANSACTIONS_SLOT_NAME,
29 THRESHOLD_CONFIG_SLOT_NAME,
30};
31use super::ProcedurePolicy;
32use crate::account::account_component_code;
33use crate::account::auth::{Approver, ApproverSet, AuthMultisig};
34use crate::procedure_root;
35
36account_component_code!(MULTISIG_SMART_CODE, "miden-standards-auth-multisig-smart.masp");
37
38const MULTISIG_SMART_LIBRARY_PATH: &str = "miden::standards::components::auth::multisig_smart";
44
45procedure_root!(
48 MULTISIG_SMART_SET_PROCEDURE_POLICY,
49 MULTISIG_SMART_LIBRARY_PATH,
50 AuthMultisigSmart::SET_PROCEDURE_POLICY_PROC_NAME,
51 AuthMultisigSmart::code()
52);
53
54static PROCEDURE_POLICIES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
61 StorageSlotName::new("miden::standards::auth::multisig_smart::procedure_policies")
62 .expect("storage slot name should be valid")
63});
64
65#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct AuthMultisigSmartConfig {
71 approver_set: ApproverSet,
72 procedure_policies: Vec<(Word, ProcedurePolicy)>,
73}
74
75impl AuthMultisigSmartConfig {
76 pub fn new(approver_set: ApproverSet) -> Self {
78 Self {
79 approver_set,
80 procedure_policies: Vec::new(),
81 }
82 }
83
84 pub fn with_proc_policies(
86 mut self,
87 proc_policies: Vec<(Word, ProcedurePolicy)>,
88 ) -> Result<Self, AccountError> {
89 validate_proc_policies(self.approver_set.approvers().len() as u32, &proc_policies)?;
90 self.procedure_policies = proc_policies;
91 Ok(self)
92 }
93
94 pub fn approver_set(&self) -> &ApproverSet {
95 &self.approver_set
96 }
97
98 pub fn approvers(&self) -> &[Approver] {
99 self.approver_set.approvers()
100 }
101
102 pub fn default_threshold(&self) -> u32 {
103 self.approver_set.threshold().get()
104 }
105
106 pub fn procedure_policies(&self) -> &[(Word, ProcedurePolicy)] {
107 &self.procedure_policies
108 }
109}
110
111fn validate_proc_policies(
112 num_approvers: u32,
113 proc_policies: &[(Word, ProcedurePolicy)],
114) -> Result<(), AccountError> {
115 let mut policy_roots = alloc::collections::BTreeSet::new();
118 for (proc_root, _) in proc_policies {
119 if !policy_roots.insert(*proc_root) {
120 return Err(AccountError::other(
121 "duplicate procedure roots are not allowed in the procedure policy map",
122 ));
123 }
124 }
125
126 for (_, policy) in proc_policies {
127 if policy.immediate_threshold() > num_approvers {
128 return Err(AccountError::other(
129 "procedure policy immediate threshold cannot exceed number of approvers",
130 ));
131 }
132 if let Some(delay_threshold) = policy.delay_threshold()
133 && delay_threshold > num_approvers
134 {
135 return Err(AccountError::other(
136 "procedure policy delay threshold cannot exceed number of approvers",
137 ));
138 }
139 }
140
141 Ok(())
142}
143
144#[derive(Debug)]
157pub struct AuthMultisigSmart {
158 config: AuthMultisigSmartConfig,
159}
160
161impl AuthMultisigSmart {
162 pub const NAME: &'static str = "miden::standards::auth::multisig_smart";
164
165 const SET_PROCEDURE_POLICY_PROC_NAME: &'static str = "set_procedure_policy";
167
168 pub fn code() -> &'static AccountComponentCode {
170 &MULTISIG_SMART_CODE
171 }
172
173 pub fn set_procedure_policy_root() -> AccountProcedureRoot {
175 *MULTISIG_SMART_SET_PROCEDURE_POLICY
176 }
177
178 pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
180 validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
181 Ok(Self { config })
182 }
183
184 pub fn threshold_config_slot() -> &'static StorageSlotName {
185 &THRESHOLD_CONFIG_SLOT_NAME
186 }
187
188 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
189 &APPROVER_PUBKEYS_SLOT_NAME
190 }
191
192 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
193 &APPROVER_SCHEME_ID_SLOT_NAME
194 }
195
196 pub fn executed_transactions_slot() -> &'static StorageSlotName {
197 &EXECUTED_TRANSACTIONS_SLOT_NAME
198 }
199
200 pub fn procedure_policies_slot() -> &'static StorageSlotName {
201 &PROCEDURE_POLICIES_SLOT_NAME
202 }
203
204 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
205 AuthMultisig::threshold_config_slot_schema()
206 }
207
208 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
209 AuthMultisig::approver_public_keys_slot_schema()
210 }
211
212 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
213 AuthMultisig::approver_auth_scheme_slot_schema()
214 }
215
216 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
217 AuthMultisig::executed_transactions_slot_schema()
218 }
219
220 pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
221 (
222 Self::procedure_policies_slot().clone(),
223 StorageSlotSchema::map(
224 "Procedure policies",
225 SchemaType::native_word(),
226 SchemaType::native_word(),
227 ),
228 )
229 }
230}
231
232impl From<AuthMultisigSmart> for AccountComponent {
233 fn from(multisig: AuthMultisigSmart) -> Self {
234 let mut storage_slots = Vec::with_capacity(5);
235
236 let num_approvers = multisig.config.approvers().len() as u32;
238 storage_slots.push(StorageSlot::with_value(
239 AuthMultisigSmart::threshold_config_slot().clone(),
240 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
241 ));
242
243 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
245 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
246 });
247 storage_slots.push(StorageSlot::with_map(
248 AuthMultisigSmart::approver_public_keys_slot().clone(),
249 StorageMap::with_entries(map_entries).unwrap(),
250 ));
251
252 let scheme_id_entries =
254 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
255 (
256 StorageMapKey::from_index(i as u32),
257 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
258 )
259 });
260 storage_slots.push(StorageSlot::with_map(
261 AuthMultisigSmart::approver_scheme_ids_slot().clone(),
262 StorageMap::with_entries(scheme_id_entries).unwrap(),
263 ));
264
265 storage_slots.push(StorageSlot::with_map(
267 AuthMultisigSmart::executed_transactions_slot().clone(),
268 StorageMap::default(),
269 ));
270
271 let procedure_policies =
273 StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
274 |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
275 ))
276 .unwrap();
277 storage_slots.push(StorageSlot::with_map(
278 AuthMultisigSmart::procedure_policies_slot().clone(),
279 procedure_policies,
280 ));
281
282 let storage_schema = StorageSchema::new(vec![
283 AuthMultisigSmart::threshold_config_slot_schema(),
284 AuthMultisigSmart::approver_public_keys_slot_schema(),
285 AuthMultisigSmart::approver_auth_scheme_slot_schema(),
286 AuthMultisigSmart::executed_transactions_slot_schema(),
287 AuthMultisigSmart::procedure_policies_slot_schema(),
288 ])
289 .expect("storage schema should be valid");
290
291 let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
292 .with_description("Multisig smart authentication component")
293 .with_storage_schema(storage_schema);
294
295 AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
296 "multisig smart component should satisfy the requirements of a valid account component",
297 )
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use alloc::string::ToString;
304
305 use miden_protocol::account::AccountBuilder;
306 use miden_protocol::account::auth::AuthSecretKey;
307
308 use super::*;
309 use crate::account::wallets::BasicWallet;
310
311 #[test]
312 fn test_multisig_smart_component_setup() {
313 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
314 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
315 let approvers = vec![
316 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
317 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
318 ];
319 let num_approvers = approvers.len() as u32;
320 let default_threshold = 2u32;
321 let receive_asset_immediate_threshold = 1u32;
322
323 let approver_set =
324 ApproverSet::new(approvers, default_threshold).expect("invalid approver set");
325 let config = AuthMultisigSmartConfig::new(approver_set)
326 .with_proc_policies(vec![(
327 BasicWallet::receive_asset_root().as_word(),
328 ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
329 .expect("procedure policy should be valid"),
330 )])
331 .expect("procedure policy config should be valid");
332
333 let component =
334 AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
335
336 let account = AccountBuilder::new([0; 32])
337 .with_component(component)
338 .with_component(BasicWallet)
339 .build()
340 .expect("account building failed");
341
342 let threshold_config = account
343 .storage()
344 .get_item(AuthMultisigSmart::threshold_config_slot())
345 .expect("threshold config should be present");
346 assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
347
348 let receive_asset_policy = account
349 .storage()
350 .get_map_item(
351 AuthMultisigSmart::procedure_policies_slot(),
352 StorageMapKey::from_raw(BasicWallet::receive_asset_root().as_word()),
353 )
354 .expect("receive_asset policy should be present");
355 assert_eq!(
356 receive_asset_policy,
357 Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
358 );
359 }
360
361 #[test]
362 fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
363 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
364 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
365 let approvers = vec![
366 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
367 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
368 ];
369
370 let receive_asset_root = BasicWallet::receive_asset_root().as_word();
371 let policy_one =
372 ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
373 let policy_two =
374 ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
375
376 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
377 let result = AuthMultisigSmartConfig::new(approver_set).with_proc_policies(vec![
378 (receive_asset_root, policy_one),
379 (receive_asset_root, policy_two),
380 ]);
381
382 assert!(
383 result
384 .unwrap_err()
385 .to_string()
386 .contains("duplicate procedure roots are not allowed in the procedure policy map")
387 );
388 }
389}