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