Skip to main content

miden_standards/note/
blocklist_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::{BLOCKLIST_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
26
27// NOTE SCRIPT
28// ================================================================================================
29
30/// Path to the BLOCKLIST_CONFIG note script procedure in the standards library.
31const BLOCKLIST_CONFIG_SCRIPT_PATH: &str = "::miden::standards::notes::blocklist_config::main";
32
33// Initialize the BLOCKLIST_CONFIG note script only once.
34static BLOCKLIST_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35    let standards_lib = StandardsLib::default();
36    let path = Path::new(BLOCKLIST_CONFIG_SCRIPT_PATH);
37    NoteScript::from_package_reference(standards_lib.as_ref(), path)
38        .expect("Standards library contains BLOCKLIST_CONFIG note script procedure")
39});
40
41// BLOCKLIST CONFIG
42// ================================================================================================
43
44/// A management action of the
45/// [`BlocklistManager`](crate::account::policies::BlocklistManager) component that a
46/// [`BlocklistConfigNote`] 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 `BlocklistManager`
51/// procedures authorize the sender through the account-wide `Authority` component.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum BlocklistConfig {
54    /// Add `account` to the blocklist. Blocking an already blocked account is a noop.
55    BlockAccount { account: AccountId },
56    /// Remove `account` from the blocklist. Unblocking an account that is not blocked is a noop.
57    UnblockAccount { account: AccountId },
58}
59
60impl BlocklistConfig {
61    // SELECTORS
62    // --------------------------------------------------------------------------------------------
63
64    // Config note selectors stored in the first storage item. Keep in sync with
65    // `blocklist_config.masm`.
66    const SELECTOR_BLOCK_ACCOUNT: u8 = 0;
67    const SELECTOR_UNBLOCK_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            BlocklistConfig::BlockAccount { .. } => Self::SELECTOR_BLOCK_ACCOUNT,
73            BlocklistConfig::UnblockAccount { .. } => Self::SELECTOR_UNBLOCK_ACCOUNT,
74        }
75    }
76
77    /// Returns the account the action operates on.
78    const fn target(self) -> AccountId {
79        match self {
80            BlocklistConfig::BlockAccount { account }
81            | BlocklistConfig::UnblockAccount { 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<BlocklistConfig> for NoteStorage {
94    fn from(config: BlocklistConfig) -> Self {
95        NoteStorage::new(config.to_storage_values())
96            .expect("number of storage items should not exceed max storage items")
97    }
98}
99
100// BLOCKLIST CONFIG NOTE
101// ================================================================================================
102
103/// A BlocklistConfig note: triggers a
104/// [`BlocklistManager`](crate::account::policies::BlocklistManager) 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 (`block_account`, `unblock_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/// `BlocklistManager` component whose blocklist 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](BlocklistConfigNote::builder); convert it into a protocol
123/// [`Note`] infallibly via `Note::from`.
124#[derive(Debug, Clone)]
125pub struct BlocklistConfigNote {
126    sender: AccountId,
127    target: AccountId,
128    config: BlocklistConfig,
129    serial_number: Word,
130    attachments: NoteAttachments,
131}
132
133#[bon::bon]
134impl BlocklistConfigNote {
135    /// Builds a new [`BlocklistConfigNote`] 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: BlocklistConfig,
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 BlocklistConfig 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 BlocklistConfigNote {
175    // CONSTANTS
176    // --------------------------------------------------------------------------------------------
177
178    /// Number of storage items of a BlocklistConfig 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 BlocklistConfig note.
189    pub fn script() -> NoteScript {
190        BLOCKLIST_CONFIG_SCRIPT.clone()
191    }
192
193    /// Returns the BlocklistConfig note script root.
194    pub fn script_root() -> NoteScriptRoot {
195        BLOCKLIST_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) -> BlocklistConfig {
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: blocklist_config_note_builder::State> BlocklistConfigNoteBuilder<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: blocklist_config_note_builder::State> BlocklistConfigNoteBuilder<S>
245where
246    S::SerialNumber: blocklist_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    ) -> BlocklistConfigNoteBuilder<blocklist_config_note_builder::SetSerialNumber<S>> {
253        self.serial_number(rng.draw_word())
254    }
255}
256
257// CONVERSIONS
258// ================================================================================================
259
260impl From<BlocklistConfigNote> for Note {
261    fn from(note: BlocklistConfigNote) -> Self {
262        // BlocklistConfig 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            BlocklistConfigNote::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 BlocklistConfigNote {
280    fn consumption_cycles() -> u32 {
281        BLOCKLIST_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_blocklist_config_note() {
304        let mut rng = RandomCoin::new(Word::empty());
305        let managed = account_id(1);
306        let owner = account_id(2);
307        let blocked = account_id(3);
308
309        let note = BlocklistConfigNote::builder()
310            .sender(owner)
311            .target(managed)
312            .config(BlocklistConfig::BlockAccount { account: blocked })
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    /// `BlockAccount` storage is `[selector, account_suffix, account_prefix]`.
327    #[test]
328    fn block_account_storage_layout() {
329        let blocked = account_id(3);
330        let storage = NoteStorage::from(BlocklistConfig::BlockAccount { account: blocked });
331
332        assert_eq!(
333            storage.items(),
334            &[
335                Felt::from(BlocklistConfig::SELECTOR_BLOCK_ACCOUNT),
336                blocked.suffix(),
337                blocked.prefix().as_felt(),
338            ]
339        );
340    }
341
342    /// `UnblockAccount` storage is `[selector, account_suffix, account_prefix]`.
343    #[test]
344    fn unblock_account_storage_layout() {
345        let blocked = account_id(3);
346        let storage = NoteStorage::from(BlocklistConfig::UnblockAccount { account: blocked });
347
348        assert_eq!(
349            storage.items(),
350            &[
351                Felt::from(BlocklistConfig::SELECTOR_UNBLOCK_ACCOUNT),
352                blocked.suffix(),
353                blocked.prefix().as_felt(),
354            ]
355        );
356    }
357}