Skip to main content

miden_standards/note/config/
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) and is fixed at note creation, bound into the note commitment. The consuming
50/// account's `BlocklistManager` procedures authorize the action through the account-wide
51/// [`Authority`](crate::account::access::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    // VARIANTS
62    // --------------------------------------------------------------------------------------------
63
64    // Config note variants stored in the first storage item. Keep in sync with
65    // `blocklist_config.masm`.
66    const VARIANT_BLOCK_ACCOUNT: u8 = 0;
67    const VARIANT_UNBLOCK_ACCOUNT: u8 = 1;
68
69    /// Returns the variant encoding this action.
70    const fn variant(self) -> u8 {
71        match self {
72            BlocklistConfig::BlockAccount { .. } => Self::VARIANT_BLOCK_ACCOUNT,
73            BlocklistConfig::UnblockAccount { .. } => Self::VARIANT_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 `[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<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 the note variant in its 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`](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/// `BlocklistManager` component whose blocklist 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](BlocklistConfigNote::builder); convert it into a protocol
124/// [`Note`] infallibly via `Note::from`.
125#[derive(Debug, Clone)]
126pub struct BlocklistConfigNote {
127    sender: AccountId,
128    target: AccountId,
129    config: BlocklistConfig,
130    serial_number: Word,
131    attachments: NoteAttachments,
132}
133
134#[bon::bon]
135impl BlocklistConfigNote {
136    /// Builds a new [`BlocklistConfigNote`] 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: BlocklistConfig,
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 BlocklistConfig 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 BlocklistConfigNote {
176    // CONSTANTS
177    // --------------------------------------------------------------------------------------------
178
179    /// Number of storage items of a BlocklistConfig 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 BlocklistConfig note.
190    pub fn script() -> NoteScript {
191        BLOCKLIST_CONFIG_SCRIPT.clone()
192    }
193
194    /// Returns the BlocklistConfig note script root.
195    pub fn script_root() -> NoteScriptRoot {
196        BLOCKLIST_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) -> BlocklistConfig {
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: blocklist_config_note_builder::State> BlocklistConfigNoteBuilder<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: blocklist_config_note_builder::State> BlocklistConfigNoteBuilder<S>
247where
248    S::SerialNumber: blocklist_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    ) -> BlocklistConfigNoteBuilder<blocklist_config_note_builder::SetSerialNumber<S>> {
255        self.serial_number(rng.draw_word())
256    }
257}
258
259// CONVERSIONS
260// ================================================================================================
261
262impl From<BlocklistConfigNote> for Note {
263    fn from(note: BlocklistConfigNote) -> Self {
264        // BlocklistConfig 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            BlocklistConfigNote::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 BlocklistConfigNote {
282    fn consumption_cycles() -> u32 {
283        BLOCKLIST_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_blocklist_config_note() {
306        let mut rng = RandomCoin::new(Word::empty());
307        let managed = account_id(1);
308        let owner = account_id(2);
309        let blocked = account_id(3);
310
311        let note = BlocklistConfigNote::builder()
312            .sender(owner)
313            .target(managed)
314            .config(BlocklistConfig::BlockAccount { account: blocked })
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    /// `BlockAccount` storage is `[variant, account_suffix, account_prefix]`.
329    #[test]
330    fn block_account_storage_layout() {
331        let blocked = account_id(3);
332        let storage = NoteStorage::from(BlocklistConfig::BlockAccount { account: blocked });
333
334        assert_eq!(
335            storage.items(),
336            &[
337                Felt::from(BlocklistConfig::VARIANT_BLOCK_ACCOUNT),
338                blocked.suffix(),
339                blocked.prefix().as_felt(),
340            ]
341        );
342    }
343
344    /// `UnblockAccount` storage is `[variant, account_suffix, account_prefix]`.
345    #[test]
346    fn unblock_account_storage_layout() {
347        let blocked = account_id(3);
348        let storage = NoteStorage::from(BlocklistConfig::UnblockAccount { account: blocked });
349
350        assert_eq!(
351            storage.items(),
352            &[
353                Felt::from(BlocklistConfig::VARIANT_UNBLOCK_ACCOUNT),
354                blocked.suffix(),
355                blocked.prefix().as_felt(),
356            ]
357        );
358    }
359}