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}