miden_standards/note/config/
rbac_config.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::{AccountId, RoleSymbol};
4use miden_protocol::assembly::Path;
5use miden_protocol::crypto::rand::FeltRng;
6use miden_protocol::errors::NoteError;
7use miden_protocol::note::{
8 Note,
9 NoteAssets,
10 NoteAttachment,
11 NoteAttachments,
12 NoteRecipient,
13 NoteScript,
14 NoteScriptRoot,
15 NoteStorage,
16 NoteTag,
17 NoteType,
18 PartialNoteMetadata,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::StandardsLib;
24use crate::note::costs::{NoteConsumptionCost, RBAC_CONFIG_CONSUMPTION_CYCLES};
25use crate::note::{AccountTargetNetworkNote, NetworkAccountTarget, NumStorageItems};
26
27const RBAC_CONFIG_SCRIPT_PATH: &str = "::miden::standards::notes::rbac_config::main";
32
33static RBAC_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35 let standards_lib = StandardsLib::default();
36 let path = Path::new(RBAC_CONFIG_SCRIPT_PATH);
37 NoteScript::from_package_reference(standards_lib.as_ref(), path)
38 .expect("Standards library contains RBAC_CONFIG note script procedure")
39});
40
41#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum RbacConfig {
54 GrantRole { role: RoleSymbol, account: AccountId },
56 RevokeRole { role: RoleSymbol, account: AccountId },
59 SetRoleAdmin {
63 role: RoleSymbol,
64 admin_role: Option<RoleSymbol>,
65 },
66 RenounceRole { role: RoleSymbol },
68}
69
70impl RbacConfig {
71 const VARIANT_GRANT_ROLE: u8 = 0;
76 const VARIANT_REVOKE_ROLE: u8 = 1;
77 const VARIANT_SET_ROLE_ADMIN: u8 = 2;
78 const VARIANT_RENOUNCE_ROLE: u8 = 3;
79
80 fn to_storage_values(&self) -> Vec<Felt> {
82 match self {
83 RbacConfig::GrantRole { role, account } => {
84 vec![
85 Felt::from(Self::VARIANT_GRANT_ROLE),
86 role.as_element(),
87 account.suffix(),
88 account.prefix().as_felt(),
89 ]
90 },
91 RbacConfig::RevokeRole { role, account } => {
92 vec![
93 Felt::from(Self::VARIANT_REVOKE_ROLE),
94 role.as_element(),
95 account.suffix(),
96 account.prefix().as_felt(),
97 ]
98 },
99 RbacConfig::SetRoleAdmin { role, admin_role } => {
100 let admin_role = admin_role.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
103 vec![Felt::from(Self::VARIANT_SET_ROLE_ADMIN), role.as_element(), admin_role]
104 },
105 RbacConfig::RenounceRole { role } => {
106 vec![Felt::from(Self::VARIANT_RENOUNCE_ROLE), role.as_element()]
107 },
108 }
109 }
110}
111
112impl From<RbacConfig> for NoteStorage {
113 fn from(config: RbacConfig) -> Self {
114 NoteStorage::new(config.to_storage_values())
115 .expect("number of storage items should not exceed max storage items")
116 }
117}
118
119#[derive(Debug, Clone)]
156pub struct RbacConfigNote {
157 sender: AccountId,
158 target: AccountId,
159 config: RbacConfig,
160 serial_number: Word,
161 attachments: NoteAttachments,
162}
163
164#[bon::bon]
165impl RbacConfigNote {
166 #[builder]
180 pub fn new(
181 #[builder(field)] mut attachments: Vec<NoteAttachment>,
182 sender: AccountId,
183 target: AccountId,
184 config: RbacConfig,
185 serial_number: Word,
186 ) -> Result<Self, NoteError> {
187 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
190 NoteError::other_with_source(
191 "failed to bind the RbacConfig note to its target account",
192 err,
193 )
194 })?;
195 let attachments = NoteAttachments::new(attachments)?;
196
197 Ok(Self {
198 sender,
199 target,
200 config,
201 serial_number,
202 attachments,
203 })
204 }
205}
206
207impl RbacConfigNote {
208 pub const NUM_STORAGE_ITEMS: NumStorageItems = NumStorageItems::Range { min: 2, max: 4 };
218
219 pub fn script() -> NoteScript {
224 RBAC_CONFIG_SCRIPT.clone()
225 }
226
227 pub fn script_root() -> NoteScriptRoot {
229 RBAC_CONFIG_SCRIPT.root()
230 }
231
232 pub fn sender(&self) -> AccountId {
234 self.sender
235 }
236
237 pub fn target(&self) -> AccountId {
239 self.target
240 }
241
242 pub fn config(&self) -> &RbacConfig {
244 &self.config
245 }
246
247 pub fn serial_number(&self) -> Word {
249 self.serial_number
250 }
251
252 pub fn attachments(&self) -> &NoteAttachments {
255 &self.attachments
256 }
257}
258
259impl<S: rbac_config_note_builder::State> RbacConfigNoteBuilder<S> {
263 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
265 self.attachments.push(attachment.into());
266 self
267 }
268
269 pub fn attachments(
271 mut self,
272 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
273 ) -> Self {
274 self.attachments.extend(attachments.into_iter().map(Into::into));
275 self
276 }
277}
278
279impl<S: rbac_config_note_builder::State> RbacConfigNoteBuilder<S>
280where
281 S::SerialNumber: rbac_config_note_builder::IsUnset,
282{
283 pub fn generate_serial_number(
285 self,
286 rng: &mut impl FeltRng,
287 ) -> RbacConfigNoteBuilder<rbac_config_note_builder::SetSerialNumber<S>> {
288 self.serial_number(rng.draw_word())
289 }
290}
291
292impl From<RbacConfigNote> for Note {
296 fn from(note: RbacConfigNote) -> Self {
297 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
300 .with_tag(NoteTag::with_account_target(note.target));
301 let recipient = NoteRecipient::new(
302 note.serial_number,
303 RbacConfigNote::script(),
304 NoteStorage::from(note.config),
305 );
306 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
307 }
308}
309
310impl From<RbacConfigNote> for AccountTargetNetworkNote {
311 fn from(note: RbacConfigNote) -> Self {
312 AccountTargetNetworkNote::new(Note::from(note))
313 .expect("RbacConfig note is public and carries a network account target attachment")
314 }
315}
316
317impl NoteConsumptionCost for RbacConfigNote {
321 fn consumption_cycles() -> u32 {
322 RBAC_CONFIG_CONSUMPTION_CYCLES
323 }
324}
325
326#[cfg(test)]
330mod tests {
331 use assert_matches::assert_matches;
332 use miden_protocol::account::AccountType;
333 use miden_protocol::crypto::rand::RandomCoin;
334 use miden_protocol::note::NoteAttachmentScheme;
335
336 use super::*;
337 use crate::note::{NetworkAccountTargetError, NetworkNoteExt, NoteExecutionHint};
338
339 fn account_id(seed: u8) -> AccountId {
340 typed_account_id(seed, AccountType::Public)
341 }
342
343 fn typed_account_id(seed: u8, account_type: AccountType) -> AccountId {
344 AccountId::builder().account_type(account_type).build_with_seed([seed; 32])
345 }
346
347 fn role(name: &str) -> RoleSymbol {
348 RoleSymbol::new(name).expect("role symbol should be valid")
349 }
350
351 #[test]
353 fn builder_builds_rbac_config_note() {
354 let mut rng = RandomCoin::new(Word::empty());
355 let managed = account_id(1);
356 let admin = account_id(2);
357 let grantee = account_id(3);
358
359 let note = RbacConfigNote::builder()
360 .sender(admin)
361 .target(managed)
362 .config(RbacConfig::GrantRole { role: role("MINTER"), account: grantee })
363 .generate_serial_number(&mut rng)
364 .build()
365 .unwrap();
366
367 assert_eq!(note.sender(), admin);
368 assert_eq!(note.target(), managed);
369
370 let note = Note::from(note);
371 assert_eq!(note.metadata().note_type(), NoteType::Public);
372 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
373 assert_eq!(note.assets().num_assets(), 0);
374 }
375
376 #[test]
379 fn builder_attaches_network_target() {
380 let mut rng = RandomCoin::new(Word::empty());
381 let managed = account_id(1);
382
383 let note = RbacConfigNote::builder()
384 .sender(account_id(2))
385 .target(managed)
386 .config(RbacConfig::RenounceRole { role: role("MINTER") })
387 .generate_serial_number(&mut rng)
388 .build()
389 .unwrap();
390
391 assert_eq!(note.attachments().num_attachments(), 1);
392
393 let network_note = AccountTargetNetworkNote::from(note);
394 assert_eq!(network_note.target_account_id(), managed);
395 assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
396 assert!(network_note.as_note().is_network_note());
397 }
398
399 #[test]
401 fn builder_keeps_caller_attachments() {
402 let mut rng = RandomCoin::new(Word::empty());
403 let managed = account_id(1);
404 let custom_scheme = NoteAttachmentScheme::new(64).unwrap();
405 let custom = NoteAttachment::with_word(custom_scheme, Word::from([7u32, 0, 0, 0]));
406
407 let note = RbacConfigNote::builder()
408 .attachment(custom.clone())
409 .sender(account_id(2))
410 .target(managed)
411 .config(RbacConfig::RenounceRole { role: role("MINTER") })
412 .generate_serial_number(&mut rng)
413 .build()
414 .unwrap();
415
416 assert_eq!(note.attachments().num_attachments(), 2);
418 assert_eq!(note.attachments().get(0), Some(&custom));
419
420 let network_note = AccountTargetNetworkNote::from(note);
421 assert_eq!(network_note.target_account_id(), managed);
422 }
423
424 #[test]
427 fn builder_rejects_target_for_other_account() {
428 let mut rng = RandomCoin::new(Word::empty());
429 let rogue_target =
430 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::None).unwrap();
431
432 let err = RbacConfigNote::builder()
433 .attachment(rogue_target)
434 .sender(account_id(2))
435 .target(account_id(1))
436 .config(RbacConfig::RenounceRole { role: role("MINTER") })
437 .generate_serial_number(&mut rng)
438 .build()
439 .unwrap_err();
440
441 assert_matches!(err, NoteError::Other { source, .. } => {
442 assert_matches!(
443 *source.unwrap().downcast().unwrap(),
444 NetworkAccountTargetError::TargetMismatch { .. }
445 )
446 });
447 }
448
449 #[test]
451 fn builder_rejects_non_public_account() {
452 let mut rng = RandomCoin::new(Word::empty());
453 let managed = typed_account_id(1, AccountType::Private);
454
455 let err = RbacConfigNote::builder()
456 .sender(account_id(2))
457 .target(managed)
458 .config(RbacConfig::RenounceRole { role: role("MINTER") })
459 .generate_serial_number(&mut rng)
460 .build()
461 .unwrap_err();
462
463 assert_matches!(err, NoteError::Other { source, .. } => {
464 assert_matches!(
465 *source.unwrap().downcast().unwrap(),
466 NetworkAccountTargetError::TargetNotPublic { .. }
467 )
468 });
469 }
470
471 #[test]
473 fn grant_role_storage_layout() {
474 let grantee = account_id(3);
475 let minter = role("MINTER");
476 let storage =
477 NoteStorage::from(RbacConfig::GrantRole { role: minter.clone(), account: grantee });
478
479 assert_eq!(
480 storage.items(),
481 &[
482 Felt::from(RbacConfig::VARIANT_GRANT_ROLE),
483 minter.as_element(),
484 grantee.suffix(),
485 grantee.prefix().as_felt(),
486 ]
487 );
488 }
489
490 #[test]
492 fn set_role_admin_default_storage_layout() {
493 let minter = role("MINTER");
494 let storage =
495 NoteStorage::from(RbacConfig::SetRoleAdmin { role: minter.clone(), admin_role: None });
496
497 assert_eq!(
498 storage.items(),
499 &[Felt::from(RbacConfig::VARIANT_SET_ROLE_ADMIN), minter.as_element(), Felt::ZERO]
500 );
501 }
502
503 #[test]
505 fn set_role_admin_delegated_storage_layout() {
506 let minter = role("MINTER");
507 let admin = role("MINT_ADMIN");
508 let storage = NoteStorage::from(RbacConfig::SetRoleAdmin {
509 role: minter.clone(),
510 admin_role: Some(admin.clone()),
511 });
512
513 assert_eq!(
514 storage.items(),
515 &[
516 Felt::from(RbacConfig::VARIANT_SET_ROLE_ADMIN),
517 minter.as_element(),
518 admin.as_element(),
519 ]
520 );
521 }
522
523 #[test]
525 fn renounce_role_storage_layout() {
526 let minter = role("MINTER");
527 let storage = NoteStorage::from(RbacConfig::RenounceRole { role: minter.clone() });
528
529 assert_eq!(
530 storage.items(),
531 &[Felt::from(RbacConfig::VARIANT_RENOUNCE_ROLE), minter.as_element()]
532 );
533 }
534}