miden_standards/note/config/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) and is
49/// fixed at note creation, bound into the note commitment. The consuming account's
50/// `PausableManager` procedures authorize the action through the account-wide
51/// [`Authority`](crate::account::access::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 // VARIANTS
62 // --------------------------------------------------------------------------------------------
63
64 // Config note variants stored in the first storage item. Keep in sync with
65 // `pause_config.masm`.
66 const VARIANT_PAUSE: u8 = 0;
67 const VARIANT_UNPAUSE: u8 = 1;
68
69 /// Returns the note storage values encoding this action, laid out as `[variant]`.
70 fn to_storage_values(self) -> Vec<Felt> {
71 match self {
72 PauseConfig::Pause => vec![Felt::from(Self::VARIANT_PAUSE)],
73 PauseConfig::Unpause => vec![Felt::from(Self::VARIANT_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 the note variant in its storage to one of the component's
93/// admin procedures (`pause`, `unpause`). Authorization is enforced by those procedures through
94/// the account-wide [`Authority`](crate::account::access::Authority) component, so the note carries
95/// no assets.
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.
99///
100/// The note is bound to the target `account` by a
101/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts
102/// that the consuming account matches that target before dispatching, so the note cannot be
103/// consumed by a third-party account that merely accepts its sender.
104///
105/// The note must be public: the script rejects a non-public note. See
106/// [the module docs](crate::note::config#note-type) for the layers that enforce it.
107///
108/// Construct one with the [builder](PauseConfigNote::builder); convert it into a protocol [`Note`]
109/// infallibly via `Note::from`.
110#[derive(Debug, Clone)]
111pub struct PauseConfigNote {
112 sender: AccountId,
113 target: AccountId,
114 config: PauseConfig,
115 serial_number: Word,
116 attachments: NoteAttachments,
117}
118
119#[bon::bon]
120impl PauseConfigNote {
121 /// Builds a new [`PauseConfigNote`] that applies `config` to `account`.
122 ///
123 /// # Errors
124 ///
125 /// Returns an error if:
126 /// - `account` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
127 /// which requires a public target).
128 /// - the attachments carry a `NetworkAccountTarget` for an account other than `account`.
129 /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
130 /// attachment occupies one of the available slots when the caller does not supply it.
131 #[builder]
132 pub fn new(
133 #[builder(field)] mut attachments: Vec<NoteAttachment>,
134 sender: AccountId,
135 target: AccountId,
136 config: PauseConfig,
137 serial_number: Word,
138 ) -> Result<Self, NoteError> {
139 // The note script asserts that the consuming account matches this target before
140 // dispatching.
141 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
142 NoteError::other_with_source(
143 "failed to bind the PauseConfig note to its target account",
144 err,
145 )
146 })?;
147
148 let attachments = NoteAttachments::new(attachments)?;
149
150 Ok(Self {
151 sender,
152 target,
153 config,
154 serial_number,
155 attachments,
156 })
157 }
158}
159
160impl PauseConfigNote {
161 // CONSTANTS
162 // --------------------------------------------------------------------------------------------
163
164 /// Number of storage items of a PauseConfig note: a single variant.
165 pub const NUM_STORAGE_ITEMS: usize = 1;
166
167 // PUBLIC ACCESSORS
168 // --------------------------------------------------------------------------------------------
169
170 /// Returns the script of the PauseConfig note.
171 pub fn script() -> NoteScript {
172 PAUSE_CONFIG_SCRIPT.clone()
173 }
174
175 /// Returns the PauseConfig note script root.
176 pub fn script_root() -> NoteScriptRoot {
177 PAUSE_CONFIG_SCRIPT.root()
178 }
179
180 /// Returns the account ID of the note's sender (the authorizing party under an owner- or
181 /// role-controlled `Authority`).
182 pub fn sender(&self) -> AccountId {
183 self.sender
184 }
185
186 /// Returns the account ID of the managed account (the account the note is tagged for).
187 pub fn target(&self) -> AccountId {
188 self.target
189 }
190
191 /// Returns the admin action carried by the note.
192 pub fn config(&self) -> PauseConfig {
193 self.config
194 }
195
196 /// Returns the note's serial number.
197 pub fn serial_number(&self) -> Word {
198 self.serial_number
199 }
200
201 /// Returns the attachments carried by the note.
202 pub fn attachments(&self) -> &NoteAttachments {
203 &self.attachments
204 }
205}
206
207// BUILDER EXTENSIONS
208// ================================================================================================
209
210impl<S: pause_config_note_builder::State> PauseConfigNoteBuilder<S> {
211 /// Adds a single attachment to the note.
212 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
213 self.attachments.push(attachment.into());
214 self
215 }
216
217 /// Adds multiple attachments to the note.
218 pub fn attachments(
219 mut self,
220 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
221 ) -> Self {
222 self.attachments.extend(attachments.into_iter().map(Into::into));
223 self
224 }
225}
226
227impl<S: pause_config_note_builder::State> PauseConfigNoteBuilder<S>
228where
229 S::SerialNumber: pause_config_note_builder::IsUnset,
230{
231 /// Draws a serial number from `rng` and sets it on the builder.
232 pub fn generate_serial_number(
233 self,
234 rng: &mut impl FeltRng,
235 ) -> PauseConfigNoteBuilder<pause_config_note_builder::SetSerialNumber<S>> {
236 self.serial_number(rng.draw_word())
237 }
238}
239
240// CONVERSIONS
241// ================================================================================================
242
243impl From<PauseConfigNote> for Note {
244 fn from(note: PauseConfigNote) -> Self {
245 // PauseConfig notes carry no assets and are always public for network execution; the action
246 // lives in the note storage.
247 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
248 .with_tag(NoteTag::with_account_target(note.target));
249 let recipient = NoteRecipient::new(
250 note.serial_number,
251 PauseConfigNote::script(),
252 NoteStorage::from(note.config),
253 );
254
255 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
256 }
257}
258
259// NOTE CONSUMPTION COST
260// ================================================================================================
261
262impl NoteConsumptionCost for PauseConfigNote {
263 fn consumption_cycles() -> u32 {
264 PAUSE_CONFIG_CONSUMPTION_CYCLES
265 }
266}
267
268// TESTS
269// ================================================================================================
270
271#[cfg(test)]
272mod tests {
273 use miden_protocol::account::AccountType;
274 use miden_protocol::crypto::rand::RandomCoin;
275
276 use super::*;
277
278 fn account_id(seed: u8) -> AccountId {
279 AccountId::builder()
280 .account_type(AccountType::Public)
281 .build_with_seed([seed; 32])
282 }
283
284 /// The builder produces a public, asset-less note tagged for the managed account.
285 #[test]
286 fn builder_builds_pause_config_note() {
287 let mut rng = RandomCoin::new(Word::empty());
288 let managed = account_id(1);
289 let sender = account_id(2);
290
291 let note = PauseConfigNote::builder()
292 .sender(sender)
293 .target(managed)
294 .config(PauseConfig::Pause)
295 .generate_serial_number(&mut rng)
296 .build()
297 .unwrap();
298
299 assert_eq!(note.sender(), sender);
300 assert_eq!(note.target(), managed);
301
302 let note = Note::from(note);
303 assert_eq!(note.metadata().note_type(), NoteType::Public);
304 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
305 assert_eq!(note.assets().num_assets(), 0);
306 }
307
308 /// `Pause` / `Unpause` storage is a single variant item.
309 #[test]
310 fn action_storage_layout() {
311 let pause = NoteStorage::from(PauseConfig::Pause);
312 assert_eq!(pause.items(), &[Felt::from(PauseConfig::VARIANT_PAUSE)]);
313
314 let unpause = NoteStorage::from(PauseConfig::Unpause);
315 assert_eq!(unpause.items(), &[Felt::from(PauseConfig::VARIANT_UNPAUSE)]);
316 }
317}