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)]
151pub struct AuthMultisigSmart {
152 config: AuthMultisigSmartConfig,
153}
154
155impl AuthMultisigSmart {
156 pub const NAME: &'static str = "miden::standards::auth::multisig_smart";
158
159 const SET_PROCEDURE_POLICY_PROC_NAME: &'static str = "set_procedure_policy";
161
162 pub fn code() -> &'static AccountComponentCode {
164 &MULTISIG_SMART_CODE
165 }
166
167 pub fn set_procedure_policy_root() -> AccountProcedureRoot {
169 *MULTISIG_SMART_SET_PROCEDURE_POLICY
170 }
171
172 pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
174 validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
175 Ok(Self { config })
176 }
177
178 pub fn threshold_config_slot() -> &'static StorageSlotName {
179 &THRESHOLD_CONFIG_SLOT_NAME
180 }
181
182 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
183 &APPROVER_PUBKEYS_SLOT_NAME
184 }
185
186 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
187 &APPROVER_SCHEME_ID_SLOT_NAME
188 }
189
190 pub fn executed_transactions_slot() -> &'static StorageSlotName {
191 &EXECUTED_TRANSACTIONS_SLOT_NAME
192 }
193
194 pub fn procedure_policies_slot() -> &'static StorageSlotName {
195 &PROCEDURE_POLICIES_SLOT_NAME
196 }
197
198 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
199 AuthMultisig::threshold_config_slot_schema()
200 }
201
202 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
203 AuthMultisig::approver_public_keys_slot_schema()
204 }
205
206 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
207 AuthMultisig::approver_auth_scheme_slot_schema()
208 }
209
210 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
211 AuthMultisig::executed_transactions_slot_schema()
212 }
213
214 pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
215 (
216 Self::procedure_policies_slot().clone(),
217 StorageSlotSchema::map(
218 "Procedure policies",
219 SchemaType::native_word(),
220 SchemaType::native_word(),
221 ),
222 )
223 }
224}
225
226impl From<AuthMultisigSmart> for AccountComponent {
227 fn from(multisig: AuthMultisigSmart) -> Self {
228 let mut storage_slots = Vec::with_capacity(5);
229
230 let num_approvers = multisig.config.approvers().len() as u32;
232 storage_slots.push(StorageSlot::with_value(
233 AuthMultisigSmart::threshold_config_slot().clone(),
234 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
235 ));
236
237 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
239 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
240 });
241 storage_slots.push(StorageSlot::with_map(
242 AuthMultisigSmart::approver_public_keys_slot().clone(),
243 StorageMap::with_entries(map_entries).unwrap(),
244 ));
245
246 let scheme_id_entries =
248 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
249 (
250 StorageMapKey::from_index(i as u32),
251 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
252 )
253 });
254 storage_slots.push(StorageSlot::with_map(
255 AuthMultisigSmart::approver_scheme_ids_slot().clone(),
256 StorageMap::with_entries(scheme_id_entries).unwrap(),
257 ));
258
259 storage_slots.push(StorageSlot::with_map(
261 AuthMultisigSmart::executed_transactions_slot().clone(),
262 StorageMap::default(),
263 ));
264
265 let procedure_policies =
267 StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
268 |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
269 ))
270 .unwrap();
271 storage_slots.push(StorageSlot::with_map(
272 AuthMultisigSmart::procedure_policies_slot().clone(),
273 procedure_policies,
274 ));
275
276 let storage_schema = StorageSchema::new(vec![
277 AuthMultisigSmart::threshold_config_slot_schema(),
278 AuthMultisigSmart::approver_public_keys_slot_schema(),
279 AuthMultisigSmart::approver_auth_scheme_slot_schema(),
280 AuthMultisigSmart::executed_transactions_slot_schema(),
281 AuthMultisigSmart::procedure_policies_slot_schema(),
282 ])
283 .expect("storage schema should be valid");
284
285 let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
286 .with_description("Multisig smart authentication component")
287 .with_storage_schema(storage_schema);
288
289 AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
290 "multisig smart component should satisfy the requirements of a valid account component",
291 )
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use alloc::string::ToString;
298
299 use miden_protocol::account::AccountBuilder;
300 use miden_protocol::account::auth::AuthSecretKey;
301
302 use super::*;
303 use crate::account::wallets::BasicWallet;
304
305 #[test]
306 fn test_multisig_smart_component_setup() {
307 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
308 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
309 let approvers = vec![
310 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
311 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
312 ];
313 let num_approvers = approvers.len() as u32;
314 let default_threshold = 2u32;
315 let receive_asset_immediate_threshold = 1u32;
316
317 let approver_set =
318 ApproverSet::new(approvers, default_threshold).expect("invalid approver set");
319 let config = AuthMultisigSmartConfig::new(approver_set)
320 .with_proc_policies(vec![(
321 BasicWallet::receive_asset_root().as_word(),
322 ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
323 .expect("procedure policy should be valid"),
324 )])
325 .expect("procedure policy config should be valid");
326
327 let component =
328 AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
329
330 let account = AccountBuilder::new([0; 32])
331 .with_component(component)
332 .with_component(BasicWallet)
333 .build()
334 .expect("account building failed");
335
336 let threshold_config = account
337 .storage()
338 .get_item(AuthMultisigSmart::threshold_config_slot())
339 .expect("threshold config should be present");
340 assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
341
342 let receive_asset_policy = account
343 .storage()
344 .get_map_item(
345 AuthMultisigSmart::procedure_policies_slot(),
346 StorageMapKey::from_raw(BasicWallet::receive_asset_root().as_word()),
347 )
348 .expect("receive_asset policy should be present");
349 assert_eq!(
350 receive_asset_policy,
351 Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
352 );
353 }
354
355 #[test]
356 fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
357 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
358 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
359 let approvers = vec![
360 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
361 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
362 ];
363
364 let receive_asset_root = BasicWallet::receive_asset_root().as_word();
365 let policy_one =
366 ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
367 let policy_two =
368 ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
369
370 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
371 let result = AuthMultisigSmartConfig::new(approver_set).with_proc_policies(vec![
372 (receive_asset_root, policy_one),
373 (receive_asset_root, policy_two),
374 ]);
375
376 assert!(
377 result
378 .unwrap_err()
379 .to_string()
380 .contains("duplicate procedure roots are not allowed in the procedure policy map")
381 );
382 }
383}