Skip to main content

miden_standards/note/
faucet_policy_action.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::{AccountId, AccountProcedureRoot};
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 FAUCET_POLICY_ACTION note script procedure in the standards library.
29const FAUCET_POLICY_ACTION_SCRIPT_PATH: &str =
30    "::miden::standards::notes::faucet_policy_action::main";
31
32// Initialize the FAUCET_POLICY_ACTION note script only once.
33static FAUCET_POLICY_ACTION_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
34    let standards_lib = StandardsLib::default();
35    let path = Path::new(FAUCET_POLICY_ACTION_SCRIPT_PATH);
36    NoteScript::from_library_reference(standards_lib.as_ref(), path)
37        .expect("Standards library contains FAUCET_POLICY_ACTION note script procedure")
38});
39
40// FAUCET POLICY ACTION
41// ================================================================================================
42
43/// A policy-switch action of the
44/// [`TokenPolicyManager`](crate::account::policies::TokenPolicyManager) component that a
45/// [`FaucetPolicyActionNote`] triggers on the faucet that consumes it.
46///
47/// Each variant switches the active policy of one kind to `policy_root`, which must be a root that
48/// the manager registered as an allowed alternative for that kind (otherwise the corresponding
49/// `set_*_policy` procedure aborts). Obtain a root from a policy type, e.g.
50/// `MintPolicy::owner_only().root()` or `MintOwnerOnly::root()`.
51///
52/// The action is encoded into the note's storage (see [`NoteStorage`] conversion below). Because
53/// the storage is fixed at note creation and bound into the note commitment, the authorized party
54/// is the note sender: the consuming faucet's `TokenPolicyManager` procedures authorize the sender
55/// through the account-wide `Authority` component.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum FaucetPolicyAction {
58    /// Switch the active mint policy to `policy_root`.
59    SetMintPolicy { policy_root: AccountProcedureRoot },
60    /// Switch the active burn policy to `policy_root`.
61    SetBurnPolicy { policy_root: AccountProcedureRoot },
62    /// Switch the active send (outgoing transfer) policy to `policy_root`.
63    SetSendPolicy { policy_root: AccountProcedureRoot },
64    /// Switch the active receive (incoming transfer) policy to `policy_root`.
65    SetReceivePolicy { policy_root: AccountProcedureRoot },
66}
67
68impl FaucetPolicyAction {
69    // SELECTORS
70    // --------------------------------------------------------------------------------------------
71
72    // Action selectors stored in the first storage item. Keep in sync with
73    // `faucet_policy_action.masm`.
74    const SELECTOR_SET_MINT_POLICY: u8 = 0;
75    const SELECTOR_SET_BURN_POLICY: u8 = 1;
76    const SELECTOR_SET_SEND_POLICY: u8 = 2;
77    const SELECTOR_SET_RECEIVE_POLICY: u8 = 3;
78
79    /// Returns the selector and policy root of this action.
80    fn parts(self) -> (u8, AccountProcedureRoot) {
81        match self {
82            FaucetPolicyAction::SetMintPolicy { policy_root } => {
83                (Self::SELECTOR_SET_MINT_POLICY, policy_root)
84            },
85            FaucetPolicyAction::SetBurnPolicy { policy_root } => {
86                (Self::SELECTOR_SET_BURN_POLICY, policy_root)
87            },
88            FaucetPolicyAction::SetSendPolicy { policy_root } => {
89                (Self::SELECTOR_SET_SEND_POLICY, policy_root)
90            },
91            FaucetPolicyAction::SetReceivePolicy { policy_root } => {
92                (Self::SELECTOR_SET_RECEIVE_POLICY, policy_root)
93            },
94        }
95    }
96
97    /// Returns the note storage values encoding this action, laid out as `[selector, POLICY_ROOT]`.
98    fn to_storage_values(self) -> Vec<Felt> {
99        let (selector, policy_root) = self.parts();
100        let mut values = Vec::with_capacity(FaucetPolicyActionNote::NUM_STORAGE_ITEMS);
101        values.push(Felt::from(selector));
102        values.extend_from_slice(policy_root.as_word().as_elements());
103        values
104    }
105}
106
107impl From<FaucetPolicyAction> for NoteStorage {
108    fn from(action: FaucetPolicyAction) -> Self {
109        NoteStorage::new(action.to_storage_values())
110            .expect("number of storage items should not exceed max storage items")
111    }
112}
113
114// FAUCET POLICY ACTION NOTE
115// ================================================================================================
116
117/// A FaucetPolicyAction note: triggers a
118/// [`TokenPolicyManager`](crate::account::policies::TokenPolicyManager) policy switch on the
119/// faucet that consumes it.
120///
121/// A single note script dispatches on a selector in the note's storage to one of the component's
122/// setters (`set_mint_policy`, `set_burn_policy`, `set_send_policy`, `set_receive_policy`).
123/// Authorization is enforced by those procedures through the account-wide `Authority` component
124/// against the note sender, so the note carries no assets and its authorization is bound to
125/// `sender` at creation time.
126///
127/// The note is always public (for network execution) and tagged for `account` — the faucet
128/// carrying the `TokenPolicyManager` component whose policy is being switched. The `sender` is the
129/// account authorized for the action per the faucet's `Authority` configuration (the owner under
130/// `Authority::OwnerControlled`, or a role member under `Authority::RbacControlled`).
131///
132/// Construct one with the [builder](FaucetPolicyActionNote::builder); convert it into a protocol
133/// [`Note`] infallibly via `Note::from`.
134#[derive(Debug, Clone)]
135pub struct FaucetPolicyActionNote {
136    sender: AccountId,
137    account: AccountId,
138    action: FaucetPolicyAction,
139    serial_number: Word,
140    attachments: NoteAttachments,
141}
142
143#[bon::bon]
144impl FaucetPolicyActionNote {
145    /// Builds a new [`FaucetPolicyActionNote`] that triggers `action` on `account`.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the attachments exceed their protocol limit (see
150    /// [`NoteAttachments::new`]).
151    #[builder]
152    pub fn new(
153        #[builder(field)] attachments: Vec<NoteAttachment>,
154        sender: AccountId,
155        account: AccountId,
156        action: FaucetPolicyAction,
157        serial_number: Word,
158    ) -> Result<Self, NoteError> {
159        let attachments = NoteAttachments::new(attachments)?;
160
161        Ok(Self {
162            sender,
163            account,
164            action,
165            serial_number,
166            attachments,
167        })
168    }
169}
170
171impl FaucetPolicyActionNote {
172    // CONSTANTS
173    // --------------------------------------------------------------------------------------------
174
175    /// Number of storage items of a FaucetPolicyAction note: a selector plus the policy root word.
176    pub const NUM_STORAGE_ITEMS: usize = 5;
177
178    // PUBLIC ACCESSORS
179    // --------------------------------------------------------------------------------------------
180
181    /// Returns the script of the FaucetPolicyAction note.
182    pub fn script() -> NoteScript {
183        FAUCET_POLICY_ACTION_SCRIPT.clone()
184    }
185
186    /// Returns the FaucetPolicyAction note script root.
187    pub fn script_root() -> NoteScriptRoot {
188        FAUCET_POLICY_ACTION_SCRIPT.root()
189    }
190
191    /// Returns the account ID of the note's sender (the account authorized for the action).
192    pub fn sender(&self) -> AccountId {
193        self.sender
194    }
195
196    /// Returns the account ID of the managed faucet (the account the note is tagged for).
197    pub fn account(&self) -> AccountId {
198        self.account
199    }
200
201    /// Returns the policy-switch action carried by the note.
202    pub fn action(&self) -> FaucetPolicyAction {
203        self.action
204    }
205
206    /// Returns the note's serial number.
207    pub fn serial_number(&self) -> Word {
208        self.serial_number
209    }
210
211    /// Returns the attachments carried by the note.
212    pub fn attachments(&self) -> &NoteAttachments {
213        &self.attachments
214    }
215}
216
217// BUILDER EXTENSIONS
218// ================================================================================================
219
220impl<S: faucet_policy_action_note_builder::State> FaucetPolicyActionNoteBuilder<S> {
221    /// Adds a single attachment to the note.
222    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
223        self.attachments.push(attachment.into());
224        self
225    }
226
227    /// Adds multiple attachments to the note.
228    pub fn attachments(
229        mut self,
230        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
231    ) -> Self {
232        self.attachments.extend(attachments.into_iter().map(Into::into));
233        self
234    }
235}
236
237impl<S: faucet_policy_action_note_builder::State> FaucetPolicyActionNoteBuilder<S>
238where
239    S::SerialNumber: faucet_policy_action_note_builder::IsUnset,
240{
241    /// Draws a serial number from `rng` and sets it on the builder.
242    pub fn generate_serial_number(
243        self,
244        rng: &mut impl FeltRng,
245    ) -> FaucetPolicyActionNoteBuilder<faucet_policy_action_note_builder::SetSerialNumber<S>> {
246        self.serial_number(rng.draw_word())
247    }
248}
249
250// CONVERSIONS
251// ================================================================================================
252
253impl From<FaucetPolicyActionNote> for Note {
254    fn from(note: FaucetPolicyActionNote) -> Self {
255        // FaucetPolicyAction notes carry no assets and are always public for network execution; the
256        // action and its policy root live in the note storage.
257        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
258            .with_tag(NoteTag::with_account_target(note.account));
259        let recipient = NoteRecipient::new(
260            note.serial_number,
261            FaucetPolicyActionNote::script(),
262            NoteStorage::from(note.action),
263        );
264
265        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
266    }
267}
268
269// TESTS
270// ================================================================================================
271
272#[cfg(test)]
273mod tests {
274    use miden_protocol::account::AccountType;
275    use miden_protocol::crypto::rand::RandomCoin;
276
277    use super::*;
278
279    fn account_id(seed: u8) -> AccountId {
280        AccountId::builder()
281            .account_type(AccountType::Public)
282            .build_with_seed([seed; 32])
283    }
284
285    fn policy_root(seed: u32) -> AccountProcedureRoot {
286        AccountProcedureRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
287    }
288
289    /// The builder produces a public, asset-less note tagged for the managed faucet.
290    #[test]
291    fn builder_builds_faucet_policy_action_note() {
292        let mut rng = RandomCoin::new(Word::empty());
293        let faucet = account_id(1);
294        let sender = account_id(2);
295
296        let note = FaucetPolicyActionNote::builder()
297            .sender(sender)
298            .account(faucet)
299            .action(FaucetPolicyAction::SetMintPolicy { policy_root: policy_root(10) })
300            .generate_serial_number(&mut rng)
301            .build()
302            .unwrap();
303
304        assert_eq!(note.sender(), sender);
305        assert_eq!(note.account(), faucet);
306
307        let note = Note::from(note);
308        assert_eq!(note.metadata().note_type(), NoteType::Public);
309        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
310        assert_eq!(note.assets().num_assets(), 0);
311    }
312
313    /// Storage is `[selector, POLICY_ROOT]` with the selector matching the action kind.
314    #[test]
315    fn storage_layout() {
316        let root = policy_root(10);
317
318        let cases = [
319            (
320                FaucetPolicyAction::SetMintPolicy { policy_root: root },
321                FaucetPolicyAction::SELECTOR_SET_MINT_POLICY,
322            ),
323            (
324                FaucetPolicyAction::SetBurnPolicy { policy_root: root },
325                FaucetPolicyAction::SELECTOR_SET_BURN_POLICY,
326            ),
327            (
328                FaucetPolicyAction::SetSendPolicy { policy_root: root },
329                FaucetPolicyAction::SELECTOR_SET_SEND_POLICY,
330            ),
331            (
332                FaucetPolicyAction::SetReceivePolicy { policy_root: root },
333                FaucetPolicyAction::SELECTOR_SET_RECEIVE_POLICY,
334            ),
335        ];
336
337        for (action, selector) in cases {
338            let storage = NoteStorage::from(action);
339            let mut expected = alloc::vec![Felt::from(selector)];
340            expected.extend_from_slice(root.as_word().as_elements());
341            assert_eq!(storage.items(), expected.as_slice());
342            assert_eq!(storage.items().len(), FaucetPolicyActionNote::NUM_STORAGE_ITEMS);
343        }
344    }
345}