Skip to main content

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