Skip to main content

miden_standards/note/config/
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) and is
55/// fixed at note creation, bound into the note commitment. The consuming faucet's
56/// `TokenPolicyManager` procedures authorize the action through the account-wide
57/// [`Authority`](crate::account::access::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    // VARIANTS
72    // --------------------------------------------------------------------------------------------
73
74    // Config note variants stored in the storage item after the policy root. Keep in sync with
75    // `faucet_policy_config.masm`.
76    const VARIANT_SET_MINT_POLICY: u8 = 0;
77    const VARIANT_SET_BURN_POLICY: u8 = 1;
78    const VARIANT_SET_SEND_POLICY: u8 = 2;
79    const VARIANT_SET_RECEIVE_POLICY: u8 = 3;
80
81    /// Returns the variant and policy root of this action.
82    fn parts(self) -> (u8, AccountProcedureRoot) {
83        match self {
84            FaucetPolicyConfig::SetMintPolicy { policy_root } => {
85                (Self::VARIANT_SET_MINT_POLICY, policy_root)
86            },
87            FaucetPolicyConfig::SetBurnPolicy { policy_root } => {
88                (Self::VARIANT_SET_BURN_POLICY, policy_root)
89            },
90            FaucetPolicyConfig::SetSendPolicy { policy_root } => {
91                (Self::VARIANT_SET_SEND_POLICY, policy_root)
92            },
93            FaucetPolicyConfig::SetReceivePolicy { policy_root } => {
94                (Self::VARIANT_SET_RECEIVE_POLICY, policy_root)
95            },
96        }
97    }
98
99    /// Returns the note storage values encoding this action, laid out as `[POLICY_ROOT, variant]`.
100    fn to_storage_values(self) -> Vec<Felt> {
101        let (variant, 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(variant));
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 the note variant in its 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
126/// [`Authority`](crate::account::access::Authority) component, so the note carries no assets.
127///
128/// The note is always public (for network execution) and tagged for `account` — the faucet
129/// carrying the `TokenPolicyManager` component whose policy is being switched.
130///
131/// The note is bound to the target `account` by a
132/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts
133/// that the consuming account matches that target before dispatching, so the note cannot be
134/// consumed by a third-party account that merely accepts its sender.
135///
136/// The note must be public: the script rejects a non-public note. See
137/// [the module docs](crate::note::config#note-type) for the layers that enforce it.
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 variant 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 authorizing party under an owner- or
212    /// role-controlled `Authority`).
213    pub fn sender(&self) -> AccountId {
214        self.sender
215    }
216
217    /// Returns the account ID of the managed faucet (the account the note is tagged for).
218    pub fn target(&self) -> AccountId {
219        self.target
220    }
221
222    /// Returns the policy-switch action carried by the note.
223    pub fn config(&self) -> FaucetPolicyConfig {
224        self.config
225    }
226
227    /// Returns the note's serial number.
228    pub fn serial_number(&self) -> Word {
229        self.serial_number
230    }
231
232    /// Returns the attachments carried by the note.
233    pub fn attachments(&self) -> &NoteAttachments {
234        &self.attachments
235    }
236}
237
238// BUILDER EXTENSIONS
239// ================================================================================================
240
241impl<S: faucet_policy_config_note_builder::State> FaucetPolicyConfigNoteBuilder<S> {
242    /// Adds a single attachment to the note.
243    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
244        self.attachments.push(attachment.into());
245        self
246    }
247
248    /// Adds multiple attachments to the note.
249    pub fn attachments(
250        mut self,
251        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
252    ) -> Self {
253        self.attachments.extend(attachments.into_iter().map(Into::into));
254        self
255    }
256}
257
258impl<S: faucet_policy_config_note_builder::State> FaucetPolicyConfigNoteBuilder<S>
259where
260    S::SerialNumber: faucet_policy_config_note_builder::IsUnset,
261{
262    /// Draws a serial number from `rng` and sets it on the builder.
263    pub fn generate_serial_number(
264        self,
265        rng: &mut impl FeltRng,
266    ) -> FaucetPolicyConfigNoteBuilder<faucet_policy_config_note_builder::SetSerialNumber<S>> {
267        self.serial_number(rng.draw_word())
268    }
269}
270
271// CONVERSIONS
272// ================================================================================================
273
274impl From<FaucetPolicyConfigNote> for Note {
275    fn from(note: FaucetPolicyConfigNote) -> Self {
276        // FaucetPolicyConfig notes carry no assets and are always public for network execution; the
277        // action and its policy root live in the note storage.
278        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
279            .with_tag(NoteTag::with_account_target(note.target));
280        let recipient = NoteRecipient::new(
281            note.serial_number,
282            FaucetPolicyConfigNote::script(),
283            NoteStorage::from(note.config),
284        );
285
286        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
287    }
288}
289
290// NOTE CONSUMPTION COST
291// ================================================================================================
292
293impl NoteConsumptionCost for FaucetPolicyConfigNote {
294    fn consumption_cycles() -> u32 {
295        FAUCET_POLICY_CONFIG_CONSUMPTION_CYCLES
296    }
297}
298
299// TESTS
300// ================================================================================================
301
302#[cfg(test)]
303mod tests {
304    use miden_protocol::account::AccountType;
305    use miden_protocol::crypto::rand::RandomCoin;
306
307    use super::*;
308
309    fn account_id(seed: u8) -> AccountId {
310        AccountId::builder()
311            .account_type(AccountType::Public)
312            .build_with_seed([seed; 32])
313    }
314
315    fn policy_root(seed: u32) -> AccountProcedureRoot {
316        AccountProcedureRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
317    }
318
319    /// The builder produces a public, asset-less note tagged for the managed faucet.
320    #[test]
321    fn builder_builds_faucet_policy_config_note() {
322        let mut rng = RandomCoin::new(Word::empty());
323        let faucet = account_id(1);
324        let sender = account_id(2);
325
326        let note = FaucetPolicyConfigNote::builder()
327            .sender(sender)
328            .target(faucet)
329            .config(FaucetPolicyConfig::SetMintPolicy { policy_root: policy_root(10) })
330            .generate_serial_number(&mut rng)
331            .build()
332            .unwrap();
333
334        assert_eq!(note.sender(), sender);
335        assert_eq!(note.target(), faucet);
336
337        let note = Note::from(note);
338        assert_eq!(note.metadata().note_type(), NoteType::Public);
339        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
340        assert_eq!(note.assets().num_assets(), 0);
341    }
342
343    /// Storage is `[POLICY_ROOT, variant]` with the variant matching the action kind.
344    #[test]
345    fn storage_layout() {
346        let root = policy_root(10);
347
348        let cases = [
349            (
350                FaucetPolicyConfig::SetMintPolicy { policy_root: root },
351                FaucetPolicyConfig::VARIANT_SET_MINT_POLICY,
352            ),
353            (
354                FaucetPolicyConfig::SetBurnPolicy { policy_root: root },
355                FaucetPolicyConfig::VARIANT_SET_BURN_POLICY,
356            ),
357            (
358                FaucetPolicyConfig::SetSendPolicy { policy_root: root },
359                FaucetPolicyConfig::VARIANT_SET_SEND_POLICY,
360            ),
361            (
362                FaucetPolicyConfig::SetReceivePolicy { policy_root: root },
363                FaucetPolicyConfig::VARIANT_SET_RECEIVE_POLICY,
364            ),
365        ];
366
367        for (action, variant) in cases {
368            let storage = NoteStorage::from(action);
369            let mut expected = Vec::from(root.as_word().as_elements());
370            expected.push(Felt::from(variant));
371            assert_eq!(storage.items(), expected.as_slice());
372            assert_eq!(storage.items().len(), FaucetPolicyConfigNote::NUM_STORAGE_ITEMS);
373        }
374    }
375}