Skip to main content

miden_standards/note/
pause_config.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
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::{NoteConsumptionCost, PAUSE_CONFIG_CONSUMPTION_CYCLES};
26
27// NOTE SCRIPT
28// ================================================================================================
29
30/// Path to the PAUSE_CONFIG note script procedure in the standards library.
31const PAUSE_CONFIG_SCRIPT_PATH: &str = "::miden::standards::notes::pause_config::main";
32
33// Initialize the PAUSE_CONFIG note script only once.
34static PAUSE_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35    let standards_lib = StandardsLib::default();
36    let path = Path::new(PAUSE_CONFIG_SCRIPT_PATH);
37    NoteScript::from_package_reference(standards_lib.as_ref(), path)
38        .expect("Standards library contains PAUSE_CONFIG note script procedure")
39});
40
41// PAUSE CONFIG
42// ================================================================================================
43
44/// A management action of the
45/// [`PausableManager`](crate::account::access::pausable::PausableManager) component that a
46/// [`PauseConfigNote`] triggers on the account that consumes it.
47///
48/// The action is encoded into the note's storage (see [`NoteStorage`] conversion below). Because
49/// the storage is fixed at note creation and bound into the note commitment, the authorized party
50/// is the note sender: the consuming account's `PausableManager` procedures authorize the sender
51/// through the account-wide `Authority` component.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PauseConfig {
54    /// Pause the account, blocking pause-gated procedures until a matching unpause.
55    Pause,
56    /// Unpause the account.
57    Unpause,
58}
59
60impl PauseConfig {
61    // SELECTORS
62    // --------------------------------------------------------------------------------------------
63
64    // Config note selectors stored in the first storage item. Keep in sync with
65    // `pause_config.masm`.
66    const SELECTOR_PAUSE: u8 = 0;
67    const SELECTOR_UNPAUSE: u8 = 1;
68
69    /// Returns the note storage values encoding this action, laid out as `[selector]`.
70    fn to_storage_values(self) -> Vec<Felt> {
71        match self {
72            PauseConfig::Pause => vec![Felt::from(Self::SELECTOR_PAUSE)],
73            PauseConfig::Unpause => vec![Felt::from(Self::SELECTOR_UNPAUSE)],
74        }
75    }
76}
77
78impl From<PauseConfig> for NoteStorage {
79    fn from(config: PauseConfig) -> Self {
80        NoteStorage::new(config.to_storage_values())
81            .expect("number of storage items should not exceed max storage items")
82    }
83}
84
85// PAUSE CONFIG NOTE
86// ================================================================================================
87
88/// A PauseConfig note: triggers a
89/// [`PausableManager`](crate::account::access::pausable::PausableManager) admin action on the
90/// account that consumes it.
91///
92/// A single note script dispatches on a selector in the note's storage to one of the component's
93/// admin procedures (`pause`, `unpause`). Authorization is enforced by those procedures through
94/// the account-wide `Authority` component against the note sender, so the note carries no assets
95/// and its authorization is bound to `sender` at creation time.
96///
97/// The note is always public (for network execution) and tagged for `account` — the account
98/// carrying the `PausableManager` component whose pause state is being managed. The `sender` is
99/// the account authorized for the action per the account's `Authority` configuration (the owner
100/// under `Authority::OwnerControlled`, or a role member under `Authority::RbacControlled`).
101///
102/// The note is bound to the target `account` by a
103/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts
104/// that the consuming account matches that target before dispatching, so the note cannot be
105/// consumed by a third-party account that merely accepts its sender.
106///
107/// Construct one with the [builder](PauseConfigNote::builder); convert it into a protocol [`Note`]
108/// infallibly via `Note::from`.
109#[derive(Debug, Clone)]
110pub struct PauseConfigNote {
111    sender: AccountId,
112    target: AccountId,
113    config: PauseConfig,
114    serial_number: Word,
115    attachments: NoteAttachments,
116}
117
118#[bon::bon]
119impl PauseConfigNote {
120    /// Builds a new [`PauseConfigNote`] that applies `config` to `account`.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if:
125    /// - `account` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
126    ///   which requires a public target).
127    /// - the attachments carry a `NetworkAccountTarget` for an account other than `account`.
128    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
129    ///   attachment occupies one of the available slots when the caller does not supply it.
130    #[builder]
131    pub fn new(
132        #[builder(field)] mut attachments: Vec<NoteAttachment>,
133        sender: AccountId,
134        target: AccountId,
135        config: PauseConfig,
136        serial_number: Word,
137    ) -> Result<Self, NoteError> {
138        // The note script asserts that the consuming account matches this target before
139        // dispatching.
140        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
141            NoteError::other_with_source(
142                "failed to bind the PauseConfig note to its target account",
143                err,
144            )
145        })?;
146
147        let attachments = NoteAttachments::new(attachments)?;
148
149        Ok(Self {
150            sender,
151            target,
152            config,
153            serial_number,
154            attachments,
155        })
156    }
157}
158
159impl PauseConfigNote {
160    // CONSTANTS
161    // --------------------------------------------------------------------------------------------
162
163    /// Number of storage items of a PauseConfig note: a single selector.
164    pub const NUM_STORAGE_ITEMS: usize = 1;
165
166    // PUBLIC ACCESSORS
167    // --------------------------------------------------------------------------------------------
168
169    /// Returns the script of the PauseConfig note.
170    pub fn script() -> NoteScript {
171        PAUSE_CONFIG_SCRIPT.clone()
172    }
173
174    /// Returns the PauseConfig note script root.
175    pub fn script_root() -> NoteScriptRoot {
176        PAUSE_CONFIG_SCRIPT.root()
177    }
178
179    /// Returns the account ID of the note's sender (the account authorized for the action).
180    pub fn sender(&self) -> AccountId {
181        self.sender
182    }
183
184    /// Returns the account ID of the managed account (the account the note is tagged for).
185    pub fn account(&self) -> AccountId {
186        self.target
187    }
188
189    /// Returns the admin action carried by the note.
190    pub fn config(&self) -> PauseConfig {
191        self.config
192    }
193
194    /// Returns the note's serial number.
195    pub fn serial_number(&self) -> Word {
196        self.serial_number
197    }
198
199    /// Returns the attachments carried by the note.
200    pub fn attachments(&self) -> &NoteAttachments {
201        &self.attachments
202    }
203}
204
205// BUILDER EXTENSIONS
206// ================================================================================================
207
208impl<S: pause_config_note_builder::State> PauseConfigNoteBuilder<S> {
209    /// Adds a single attachment to the note.
210    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
211        self.attachments.push(attachment.into());
212        self
213    }
214
215    /// Adds multiple attachments to the note.
216    pub fn attachments(
217        mut self,
218        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
219    ) -> Self {
220        self.attachments.extend(attachments.into_iter().map(Into::into));
221        self
222    }
223}
224
225impl<S: pause_config_note_builder::State> PauseConfigNoteBuilder<S>
226where
227    S::SerialNumber: pause_config_note_builder::IsUnset,
228{
229    /// Draws a serial number from `rng` and sets it on the builder.
230    pub fn generate_serial_number(
231        self,
232        rng: &mut impl FeltRng,
233    ) -> PauseConfigNoteBuilder<pause_config_note_builder::SetSerialNumber<S>> {
234        self.serial_number(rng.draw_word())
235    }
236}
237
238// CONVERSIONS
239// ================================================================================================
240
241impl From<PauseConfigNote> for Note {
242    fn from(note: PauseConfigNote) -> Self {
243        // PauseConfig notes carry no assets and are always public for network execution; the action
244        // lives in the note storage.
245        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
246            .with_tag(NoteTag::with_account_target(note.target));
247        let recipient = NoteRecipient::new(
248            note.serial_number,
249            PauseConfigNote::script(),
250            NoteStorage::from(note.config),
251        );
252
253        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
254    }
255}
256
257// NOTE CONSUMPTION COST
258// ================================================================================================
259
260impl NoteConsumptionCost for PauseConfigNote {
261    fn consumption_cycles() -> u32 {
262        PAUSE_CONFIG_CONSUMPTION_CYCLES
263    }
264}
265
266// TESTS
267// ================================================================================================
268
269#[cfg(test)]
270mod tests {
271    use miden_protocol::account::AccountType;
272    use miden_protocol::crypto::rand::RandomCoin;
273
274    use super::*;
275
276    fn account_id(seed: u8) -> AccountId {
277        AccountId::builder()
278            .account_type(AccountType::Public)
279            .build_with_seed([seed; 32])
280    }
281
282    /// The builder produces a public, asset-less note tagged for the managed account.
283    #[test]
284    fn builder_builds_pause_config_note() {
285        let mut rng = RandomCoin::new(Word::empty());
286        let managed = account_id(1);
287        let sender = account_id(2);
288
289        let note = PauseConfigNote::builder()
290            .sender(sender)
291            .target(managed)
292            .config(PauseConfig::Pause)
293            .generate_serial_number(&mut rng)
294            .build()
295            .unwrap();
296
297        assert_eq!(note.sender(), sender);
298        assert_eq!(note.account(), managed);
299
300        let note = Note::from(note);
301        assert_eq!(note.metadata().note_type(), NoteType::Public);
302        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
303        assert_eq!(note.assets().num_assets(), 0);
304    }
305
306    /// `Pause` / `Unpause` storage is a single selector item.
307    #[test]
308    fn action_storage_layout() {
309        let pause = NoteStorage::from(PauseConfig::Pause);
310        assert_eq!(pause.items(), &[Felt::from(PauseConfig::SELECTOR_PAUSE)]);
311
312        let unpause = NoteStorage::from(PauseConfig::Unpause);
313        assert_eq!(unpause.items(), &[Felt::from(PauseConfig::SELECTOR_UNPAUSE)]);
314    }
315}