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