1use 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};
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 SELECTOR_GRANT_ROLE: u8 = 0;
76 const SELECTOR_REVOKE_ROLE: u8 = 1;
77 const SELECTOR_SET_ROLE_ADMIN: u8 = 2;
78 const SELECTOR_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::SELECTOR_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::SELECTOR_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::SELECTOR_SET_ROLE_ADMIN), role.as_element(), admin_role]
104 },
105 RbacConfig::RenounceRole { role } => {
106 vec![Felt::from(Self::SELECTOR_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)]
153pub struct RbacConfigNote {
154 sender: AccountId,
155 target: AccountId,
156 config: RbacConfig,
157 serial_number: Word,
158 attachments: NoteAttachments,
159}
160
161#[bon::bon]
162impl RbacConfigNote {
163 #[builder]
177 pub fn new(
178 #[builder(field)] mut attachments: Vec<NoteAttachment>,
179 sender: AccountId,
180 target: AccountId,
181 config: RbacConfig,
182 serial_number: Word,
183 ) -> Result<Self, NoteError> {
184 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
187 NoteError::other_with_source(
188 "failed to bind the RbacConfig note to its target account",
189 err,
190 )
191 })?;
192 let attachments = NoteAttachments::new(attachments)?;
193
194 Ok(Self {
195 sender,
196 target,
197 config,
198 serial_number,
199 attachments,
200 })
201 }
202}
203
204impl RbacConfigNote {
205 pub const MAX_NUM_STORAGE_ITEMS: usize = 4;
213
214 pub fn script() -> NoteScript {
219 RBAC_CONFIG_SCRIPT.clone()
220 }
221
222 pub fn script_root() -> NoteScriptRoot {
224 RBAC_CONFIG_SCRIPT.root()
225 }
226
227 pub fn sender(&self) -> AccountId {
229 self.sender
230 }
231
232 pub fn account(&self) -> AccountId {
234 self.target
235 }
236
237 pub fn config(&self) -> &RbacConfig {
239 &self.config
240 }
241
242 pub fn serial_number(&self) -> Word {
244 self.serial_number
245 }
246
247 pub fn attachments(&self) -> &NoteAttachments {
250 &self.attachments
251 }
252}
253
254impl<S: rbac_config_note_builder::State> RbacConfigNoteBuilder<S> {
258 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
260 self.attachments.push(attachment.into());
261 self
262 }
263
264 pub fn attachments(
266 mut self,
267 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
268 ) -> Self {
269 self.attachments.extend(attachments.into_iter().map(Into::into));
270 self
271 }
272}
273
274impl<S: rbac_config_note_builder::State> RbacConfigNoteBuilder<S>
275where
276 S::SerialNumber: rbac_config_note_builder::IsUnset,
277{
278 pub fn generate_serial_number(
280 self,
281 rng: &mut impl FeltRng,
282 ) -> RbacConfigNoteBuilder<rbac_config_note_builder::SetSerialNumber<S>> {
283 self.serial_number(rng.draw_word())
284 }
285}
286
287impl From<RbacConfigNote> for Note {
291 fn from(note: RbacConfigNote) -> Self {
292 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
295 .with_tag(NoteTag::with_account_target(note.target));
296 let recipient = NoteRecipient::new(
297 note.serial_number,
298 RbacConfigNote::script(),
299 NoteStorage::from(note.config),
300 );
301 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
302 }
303}
304
305impl From<RbacConfigNote> for AccountTargetNetworkNote {
306 fn from(note: RbacConfigNote) -> Self {
307 AccountTargetNetworkNote::new(Note::from(note))
308 .expect("RbacConfig note is public and carries a network account target attachment")
309 }
310}
311
312impl NoteConsumptionCost for RbacConfigNote {
316 fn consumption_cycles() -> u32 {
317 RBAC_CONFIG_CONSUMPTION_CYCLES
318 }
319}
320
321#[cfg(test)]
325mod tests {
326 use assert_matches::assert_matches;
327 use miden_protocol::account::AccountType;
328 use miden_protocol::crypto::rand::RandomCoin;
329 use miden_protocol::note::NoteAttachmentScheme;
330
331 use super::*;
332 use crate::note::{NetworkAccountTargetError, NetworkNoteExt, NoteExecutionHint};
333
334 fn account_id(seed: u8) -> AccountId {
335 typed_account_id(seed, AccountType::Public)
336 }
337
338 fn typed_account_id(seed: u8, account_type: AccountType) -> AccountId {
339 AccountId::builder().account_type(account_type).build_with_seed([seed; 32])
340 }
341
342 fn role(name: &str) -> RoleSymbol {
343 RoleSymbol::new(name).expect("role symbol should be valid")
344 }
345
346 #[test]
348 fn builder_builds_rbac_config_note() {
349 let mut rng = RandomCoin::new(Word::empty());
350 let managed = account_id(1);
351 let admin = account_id(2);
352 let grantee = account_id(3);
353
354 let note = RbacConfigNote::builder()
355 .sender(admin)
356 .target(managed)
357 .config(RbacConfig::GrantRole { role: role("MINTER"), account: grantee })
358 .generate_serial_number(&mut rng)
359 .build()
360 .unwrap();
361
362 assert_eq!(note.sender(), admin);
363 assert_eq!(note.account(), managed);
364
365 let note = Note::from(note);
366 assert_eq!(note.metadata().note_type(), NoteType::Public);
367 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
368 assert_eq!(note.assets().num_assets(), 0);
369 }
370
371 #[test]
374 fn builder_attaches_network_target() {
375 let mut rng = RandomCoin::new(Word::empty());
376 let managed = account_id(1);
377
378 let note = RbacConfigNote::builder()
379 .sender(account_id(2))
380 .target(managed)
381 .config(RbacConfig::RenounceRole { role: role("MINTER") })
382 .generate_serial_number(&mut rng)
383 .build()
384 .unwrap();
385
386 assert_eq!(note.attachments().num_attachments(), 1);
387
388 let network_note = AccountTargetNetworkNote::from(note);
389 assert_eq!(network_note.target_account_id(), managed);
390 assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
391 assert!(network_note.as_note().is_network_note());
392 }
393
394 #[test]
396 fn builder_keeps_caller_attachments() {
397 let mut rng = RandomCoin::new(Word::empty());
398 let managed = account_id(1);
399 let custom_scheme = NoteAttachmentScheme::new(64).unwrap();
400 let custom = NoteAttachment::with_word(custom_scheme, Word::from([7u32, 0, 0, 0]));
401
402 let note = RbacConfigNote::builder()
403 .attachment(custom.clone())
404 .sender(account_id(2))
405 .target(managed)
406 .config(RbacConfig::RenounceRole { role: role("MINTER") })
407 .generate_serial_number(&mut rng)
408 .build()
409 .unwrap();
410
411 assert_eq!(note.attachments().num_attachments(), 2);
413 assert_eq!(note.attachments().get(0), Some(&custom));
414
415 let network_note = AccountTargetNetworkNote::from(note);
416 assert_eq!(network_note.target_account_id(), managed);
417 }
418
419 #[test]
422 fn builder_rejects_target_for_other_account() {
423 let mut rng = RandomCoin::new(Word::empty());
424 let rogue_target =
425 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::None).unwrap();
426
427 let err = RbacConfigNote::builder()
428 .attachment(rogue_target)
429 .sender(account_id(2))
430 .target(account_id(1))
431 .config(RbacConfig::RenounceRole { role: role("MINTER") })
432 .generate_serial_number(&mut rng)
433 .build()
434 .unwrap_err();
435
436 assert_matches!(err, NoteError::Other { source, .. } => {
437 assert_matches!(
438 *source.unwrap().downcast().unwrap(),
439 NetworkAccountTargetError::TargetMismatch { .. }
440 )
441 });
442 }
443
444 #[test]
446 fn builder_rejects_non_public_account() {
447 let mut rng = RandomCoin::new(Word::empty());
448 let managed = typed_account_id(1, AccountType::Private);
449
450 let err = RbacConfigNote::builder()
451 .sender(account_id(2))
452 .target(managed)
453 .config(RbacConfig::RenounceRole { role: role("MINTER") })
454 .generate_serial_number(&mut rng)
455 .build()
456 .unwrap_err();
457
458 assert_matches!(err, NoteError::Other { source, .. } => {
459 assert_matches!(
460 *source.unwrap().downcast().unwrap(),
461 NetworkAccountTargetError::TargetNotPublic { .. }
462 )
463 });
464 }
465
466 #[test]
468 fn grant_role_storage_layout() {
469 let grantee = account_id(3);
470 let minter = role("MINTER");
471 let storage =
472 NoteStorage::from(RbacConfig::GrantRole { role: minter.clone(), account: grantee });
473
474 assert_eq!(
475 storage.items(),
476 &[
477 Felt::from(RbacConfig::SELECTOR_GRANT_ROLE),
478 minter.as_element(),
479 grantee.suffix(),
480 grantee.prefix().as_felt(),
481 ]
482 );
483 }
484
485 #[test]
487 fn set_role_admin_default_storage_layout() {
488 let minter = role("MINTER");
489 let storage =
490 NoteStorage::from(RbacConfig::SetRoleAdmin { role: minter.clone(), admin_role: None });
491
492 assert_eq!(
493 storage.items(),
494 &[Felt::from(RbacConfig::SELECTOR_SET_ROLE_ADMIN), minter.as_element(), Felt::ZERO]
495 );
496 }
497
498 #[test]
500 fn set_role_admin_delegated_storage_layout() {
501 let minter = role("MINTER");
502 let admin = role("MINT_ADMIN");
503 let storage = NoteStorage::from(RbacConfig::SetRoleAdmin {
504 role: minter.clone(),
505 admin_role: Some(admin.clone()),
506 });
507
508 assert_eq!(
509 storage.items(),
510 &[
511 Felt::from(RbacConfig::SELECTOR_SET_ROLE_ADMIN),
512 minter.as_element(),
513 admin.as_element(),
514 ]
515 );
516 }
517
518 #[test]
520 fn renounce_role_storage_layout() {
521 let minter = role("MINTER");
522 let storage = NoteStorage::from(RbacConfig::RenounceRole { role: minter.clone() });
523
524 assert_eq!(
525 storage.items(),
526 &[Felt::from(RbacConfig::SELECTOR_RENOUNCE_ROLE), minter.as_element()]
527 );
528 }
529}