Skip to main content

miden_standards/note/
rbac_action.rs

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;
24
25// NOTE SCRIPT
26// ================================================================================================
27
28/// Path to the RBAC_ACTION note script procedure in the standards library.
29const RBAC_ACTION_SCRIPT_PATH: &str = "::miden::standards::notes::rbac_action::main";
30
31// Initialize the RBAC_ACTION note script only once.
32static RBAC_ACTION_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33    let standards_lib = StandardsLib::default();
34    let path = Path::new(RBAC_ACTION_SCRIPT_PATH);
35    NoteScript::from_library_reference(standards_lib.as_ref(), path)
36        .expect("Standards library contains RBAC_ACTION note script procedure")
37});
38
39// RBAC ACTION
40// ================================================================================================
41
42/// A management action of the
43/// [`RoleBasedAccessControl`](crate::account::access::RoleBasedAccessControl) component that an
44/// [`RbacActionNote`] triggers on the account that consumes it.
45///
46/// The action, together with its arguments, is encoded into the note's storage (see
47/// [`NoteStorage`] conversion below). Because the storage is fixed at note creation and bound into
48/// the note commitment, the authorized party is the note sender: the consuming account's `rbac`
49/// procedures authorize against `active_note::get_sender`.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum RbacAction {
52    /// Grant `role` to `account`. Only a member of the role's effective admin role is authorized.
53    GrantRole { role: RoleSymbol, account: AccountId },
54    /// Revoke `role` from `account`. Only a member of the role's effective admin role is
55    /// authorized.
56    RevokeRole { role: RoleSymbol, account: AccountId },
57    /// Set the admin role of `role` to `admin_role`. A value of `None` reverts `role` to
58    /// management by the default `ADMIN` role. Only a member of the role's current effective admin
59    /// role is authorized.
60    SetRoleAdmin {
61        role: RoleSymbol,
62        admin_role: Option<RoleSymbol>,
63    },
64    /// Renounce `role` held by the note sender.
65    RenounceRole { role: RoleSymbol },
66}
67
68impl RbacAction {
69    // SELECTORS
70    // --------------------------------------------------------------------------------------------
71
72    // Action selectors stored in the first storage item. Keep in sync with `rbac_action.masm`.
73    const SELECTOR_GRANT_ROLE: u8 = 0;
74    const SELECTOR_REVOKE_ROLE: u8 = 1;
75    const SELECTOR_SET_ROLE_ADMIN: u8 = 2;
76    const SELECTOR_RENOUNCE_ROLE: u8 = 3;
77
78    /// Returns the note storage values encoding this action, laid out as `[selector, ..args]`.
79    fn to_storage_values(&self) -> Vec<Felt> {
80        match self {
81            RbacAction::GrantRole { role, account } => {
82                vec![
83                    Felt::from(Self::SELECTOR_GRANT_ROLE),
84                    role.as_element(),
85                    account.suffix(),
86                    account.prefix().as_felt(),
87                ]
88            },
89            RbacAction::RevokeRole { role, account } => {
90                vec![
91                    Felt::from(Self::SELECTOR_REVOKE_ROLE),
92                    role.as_element(),
93                    account.suffix(),
94                    account.prefix().as_felt(),
95                ]
96            },
97            RbacAction::SetRoleAdmin { role, admin_role } => {
98                // A missing admin role is encoded as 0, the value `rbac::set_role_admin` treats as
99                // "revert to the default ADMIN role".
100                let admin_role = admin_role.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
101                vec![Felt::from(Self::SELECTOR_SET_ROLE_ADMIN), role.as_element(), admin_role]
102            },
103            RbacAction::RenounceRole { role } => {
104                vec![Felt::from(Self::SELECTOR_RENOUNCE_ROLE), role.as_element()]
105            },
106        }
107    }
108}
109
110impl From<RbacAction> for NoteStorage {
111    fn from(action: RbacAction) -> Self {
112        NoteStorage::new(action.to_storage_values())
113            .expect("number of storage items should not exceed max storage items")
114    }
115}
116
117// RBAC ACTION NOTE
118// ================================================================================================
119
120/// An RbacAction note: triggers a
121/// [`RoleBasedAccessControl`](crate::account::access::RoleBasedAccessControl) management action on
122/// the account that consumes it.
123///
124/// A single note script dispatches on a selector in the note's storage to one of the component's
125/// management procedures (`grant_role`, `revoke_role`, `set_role_admin`, `renounce_role`). All
126/// authorization is enforced by those procedures against the note sender, so the note carries no
127/// assets and its authorization is bound to `sender` at creation time.
128///
129/// The note is always public (for network execution) and tagged for `account` — the account
130/// carrying the `RoleBasedAccessControl` component whose role graph is being managed. The `sender`
131/// is the account authorized for the selected action: a member of the role's effective admin role
132/// for `GrantRole` / `RevokeRole` / `SetRoleAdmin`, or the role holder itself for `RenounceRole`.
133///
134/// Construct one with the [builder](RbacActionNote::builder); convert it into a protocol [`Note`]
135/// infallibly via `Note::from`.
136#[derive(Debug, Clone)]
137pub struct RbacActionNote {
138    sender: AccountId,
139    account: AccountId,
140    action: RbacAction,
141    serial_number: Word,
142    attachments: NoteAttachments,
143}
144
145#[bon::bon]
146impl RbacActionNote {
147    /// Builds a new [`RbacActionNote`] that triggers `action` on `account`.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the attachments exceed their protocol limit (see
152    /// [`NoteAttachments::new`]).
153    #[builder]
154    pub fn new(
155        #[builder(field)] attachments: Vec<NoteAttachment>,
156        sender: AccountId,
157        account: AccountId,
158        action: RbacAction,
159        serial_number: Word,
160    ) -> Result<Self, NoteError> {
161        let attachments = NoteAttachments::new(attachments)?;
162
163        Ok(Self {
164            sender,
165            account,
166            action,
167            serial_number,
168            attachments,
169        })
170    }
171}
172
173impl RbacActionNote {
174    // CONSTANTS
175    // --------------------------------------------------------------------------------------------
176
177    /// Upper bound on the number of storage items of an RbacAction note.
178    ///
179    /// The layout is variable: `GrantRole` / `RevokeRole` use 4 items (`[selector, role_symbol,
180    /// account_suffix, account_prefix]`), `SetRoleAdmin` uses 3, and `RenounceRole` uses 2.
181    pub const MAX_NUM_STORAGE_ITEMS: usize = 4;
182
183    // PUBLIC ACCESSORS
184    // --------------------------------------------------------------------------------------------
185
186    /// Returns the script of the RbacAction note.
187    pub fn script() -> NoteScript {
188        RBAC_ACTION_SCRIPT.clone()
189    }
190
191    /// Returns the RbacAction note script root.
192    pub fn script_root() -> NoteScriptRoot {
193        RBAC_ACTION_SCRIPT.root()
194    }
195
196    /// Returns the account ID of the note's sender (the account authorized for the action).
197    pub fn sender(&self) -> AccountId {
198        self.sender
199    }
200
201    /// Returns the account ID of the managed account (the account the note is tagged for).
202    pub fn account(&self) -> AccountId {
203        self.account
204    }
205
206    /// Returns the management action carried by the note.
207    pub fn action(&self) -> &RbacAction {
208        &self.action
209    }
210
211    /// Returns the note's serial number.
212    pub fn serial_number(&self) -> Word {
213        self.serial_number
214    }
215
216    /// Returns the attachments carried by the note.
217    pub fn attachments(&self) -> &NoteAttachments {
218        &self.attachments
219    }
220}
221
222// BUILDER EXTENSIONS
223// ================================================================================================
224
225impl<S: rbac_action_note_builder::State> RbacActionNoteBuilder<S> {
226    /// Adds a single attachment to the note.
227    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
228        self.attachments.push(attachment.into());
229        self
230    }
231
232    /// Adds multiple attachments to the note.
233    pub fn attachments(
234        mut self,
235        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
236    ) -> Self {
237        self.attachments.extend(attachments.into_iter().map(Into::into));
238        self
239    }
240}
241
242impl<S: rbac_action_note_builder::State> RbacActionNoteBuilder<S>
243where
244    S::SerialNumber: rbac_action_note_builder::IsUnset,
245{
246    /// Draws a serial number from `rng` and sets it on the builder.
247    pub fn generate_serial_number(
248        self,
249        rng: &mut impl FeltRng,
250    ) -> RbacActionNoteBuilder<rbac_action_note_builder::SetSerialNumber<S>> {
251        self.serial_number(rng.draw_word())
252    }
253}
254
255// CONVERSIONS
256// ================================================================================================
257
258impl From<RbacActionNote> for Note {
259    fn from(note: RbacActionNote) -> Self {
260        // RbacAction notes carry no assets and are always public for network execution; the action
261        // and its arguments live in the note storage.
262        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
263            .with_tag(NoteTag::with_account_target(note.account));
264        let recipient = NoteRecipient::new(
265            note.serial_number,
266            RbacActionNote::script(),
267            NoteStorage::from(note.action),
268        );
269
270        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
271    }
272}
273
274// TESTS
275// ================================================================================================
276
277#[cfg(test)]
278mod tests {
279    use miden_protocol::account::AccountType;
280    use miden_protocol::crypto::rand::RandomCoin;
281
282    use super::*;
283
284    fn account_id(seed: u8) -> AccountId {
285        AccountId::builder()
286            .account_type(AccountType::Public)
287            .build_with_seed([seed; 32])
288    }
289
290    fn role(name: &str) -> RoleSymbol {
291        RoleSymbol::new(name).expect("role symbol should be valid")
292    }
293
294    /// The builder produces a public, asset-less note tagged for the managed account.
295    #[test]
296    fn builder_builds_rbac_action_note() {
297        let mut rng = RandomCoin::new(Word::empty());
298        let managed = account_id(1);
299        let admin = account_id(2);
300        let grantee = account_id(3);
301
302        let note = RbacActionNote::builder()
303            .sender(admin)
304            .account(managed)
305            .action(RbacAction::GrantRole { role: role("MINTER"), account: grantee })
306            .generate_serial_number(&mut rng)
307            .build()
308            .unwrap();
309
310        assert_eq!(note.sender(), admin);
311        assert_eq!(note.account(), managed);
312
313        let note = Note::from(note);
314        assert_eq!(note.metadata().note_type(), NoteType::Public);
315        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
316        assert_eq!(note.assets().num_assets(), 0);
317    }
318
319    /// `GrantRole` storage is `[selector, role_symbol, account_suffix, account_prefix]`.
320    #[test]
321    fn grant_role_storage_layout() {
322        let grantee = account_id(3);
323        let minter = role("MINTER");
324        let storage =
325            NoteStorage::from(RbacAction::GrantRole { role: minter.clone(), account: grantee });
326
327        assert_eq!(
328            storage.items(),
329            &[
330                Felt::from(RbacAction::SELECTOR_GRANT_ROLE),
331                minter.as_element(),
332                grantee.suffix(),
333                grantee.prefix().as_felt(),
334            ]
335        );
336    }
337
338    /// `SetRoleAdmin` with `None` encodes a zero admin role (revert to the default `ADMIN` role).
339    #[test]
340    fn set_role_admin_default_storage_layout() {
341        let minter = role("MINTER");
342        let storage =
343            NoteStorage::from(RbacAction::SetRoleAdmin { role: minter.clone(), admin_role: None });
344
345        assert_eq!(
346            storage.items(),
347            &[Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN), minter.as_element(), Felt::ZERO]
348        );
349    }
350
351    /// `SetRoleAdmin` with `Some` encodes the delegated admin role symbol.
352    #[test]
353    fn set_role_admin_delegated_storage_layout() {
354        let minter = role("MINTER");
355        let admin = role("MINT_ADMIN");
356        let storage = NoteStorage::from(RbacAction::SetRoleAdmin {
357            role: minter.clone(),
358            admin_role: Some(admin.clone()),
359        });
360
361        assert_eq!(
362            storage.items(),
363            &[
364                Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN),
365                minter.as_element(),
366                admin.as_element(),
367            ]
368        );
369    }
370
371    /// `RenounceRole` storage is `[selector, role_symbol]`.
372    #[test]
373    fn renounce_role_storage_layout() {
374        let minter = role("MINTER");
375        let storage = NoteStorage::from(RbacAction::RenounceRole { role: minter.clone() });
376
377        assert_eq!(
378            storage.items(),
379            &[Felt::from(RbacAction::SELECTOR_RENOUNCE_ROLE), minter.as_element()]
380        );
381    }
382}