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