Skip to main content

miden_standards/note/
network_account_config.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::{AccountId, AccountProcedureRoot};
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::transaction::TransactionScriptRoot;
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25use crate::note::NetworkAccountTarget;
26use crate::note::costs::{NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27
28// NOTE SCRIPT
29// ================================================================================================
30
31/// Path to the NETWORK_ACCOUNT_CONFIG note script procedure in the standards library.
32const NETWORK_ACCOUNT_CONFIG_SCRIPT_PATH: &str =
33    "::miden::standards::notes::network_account_config::main";
34
35// Initialize the NETWORK_ACCOUNT_CONFIG note script only once.
36static NETWORK_ACCOUNT_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
37    let standards_lib = StandardsLib::default();
38    let path = Path::new(NETWORK_ACCOUNT_CONFIG_SCRIPT_PATH);
39    NoteScript::from_package_reference(standards_lib.as_ref(), path)
40        .expect("Standards library contains NETWORK_ACCOUNT_CONFIG note script procedure")
41});
42
43// NETWORK ACCOUNT CONFIG
44// ================================================================================================
45
46/// A configuration action of the
47/// [`AuthNetworkAccount`](crate::account::auth::AuthNetworkAccount) component that a
48/// [`NetworkAccountConfigNote`] triggers on the network account that consumes it.
49///
50/// Each variant adds or removes one root from the note-script allowlist, the tx-script allowlist,
51/// or the allowed fee policy roots map. The allowlist checks read the transaction's initial state,
52/// so an update only takes effect from the account's next transaction.
53///
54/// The action is encoded into the note's storage (see [`NoteStorage`] conversion below). Because
55/// the storage is fixed at note creation and bound into the note commitment, the authorized party
56/// is the note sender: the consuming account's `AuthNetworkAccount` procedures authorize the sender
57/// through the account-wide `Authority` component.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum NetworkAccountConfig {
60    /// Adds `script_root` to the note script allowlist.
61    AddAllowedNoteScript { script_root: NoteScriptRoot },
62    /// Removes `script_root` from the note script allowlist.
63    RemoveAllowedNoteScript { script_root: NoteScriptRoot },
64    /// Adds `script_root` to the tx script allowlist.
65    AddAllowedTxScript { script_root: TransactionScriptRoot },
66    /// Removes `script_root` from the tx script allowlist.
67    RemoveAllowedTxScript { script_root: TransactionScriptRoot },
68    /// Adds `policy_root` to the allowed fee policy roots map.
69    AddAllowedFeePolicy { policy_root: AccountProcedureRoot },
70    /// Removes `policy_root` from the allowed fee policy roots map.
71    RemoveAllowedFeePolicy { policy_root: AccountProcedureRoot },
72}
73
74impl NetworkAccountConfig {
75    // SELECTORS
76    // --------------------------------------------------------------------------------------------
77
78    // Config note selectors stored in the first storage item. Keep in sync with
79    // `network_account_config.masm`.
80    const SELECTOR_ADD_ALLOWED_NOTE_SCRIPT: u8 = 0;
81    const SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT: u8 = 1;
82    const SELECTOR_ADD_ALLOWED_TX_SCRIPT: u8 = 2;
83    const SELECTOR_REMOVE_ALLOWED_TX_SCRIPT: u8 = 3;
84    const SELECTOR_ADD_ALLOWED_FEE_POLICY: u8 = 4;
85    const SELECTOR_REMOVE_ALLOWED_FEE_POLICY: u8 = 5;
86
87    /// Returns the selector and the affected root of this action.
88    fn parts(self) -> (u8, Word) {
89        match self {
90            NetworkAccountConfig::AddAllowedNoteScript { script_root } => {
91                (Self::SELECTOR_ADD_ALLOWED_NOTE_SCRIPT, script_root.as_word())
92            },
93            NetworkAccountConfig::RemoveAllowedNoteScript { script_root } => {
94                (Self::SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT, script_root.as_word())
95            },
96            NetworkAccountConfig::AddAllowedTxScript { script_root } => {
97                (Self::SELECTOR_ADD_ALLOWED_TX_SCRIPT, script_root.as_word())
98            },
99            NetworkAccountConfig::RemoveAllowedTxScript { script_root } => {
100                (Self::SELECTOR_REMOVE_ALLOWED_TX_SCRIPT, script_root.as_word())
101            },
102            NetworkAccountConfig::AddAllowedFeePolicy { policy_root } => {
103                (Self::SELECTOR_ADD_ALLOWED_FEE_POLICY, policy_root.as_word())
104            },
105            NetworkAccountConfig::RemoveAllowedFeePolicy { policy_root } => {
106                (Self::SELECTOR_REMOVE_ALLOWED_FEE_POLICY, policy_root.as_word())
107            },
108        }
109    }
110
111    /// Returns the note storage values encoding this action, laid out as `[ROOT, selector]`.
112    fn to_storage_values(self) -> Vec<Felt> {
113        let (selector, script_root) = self.parts();
114        let mut values = Vec::with_capacity(NetworkAccountConfigNote::NUM_STORAGE_ITEMS);
115        values.extend_from_slice(script_root.as_elements());
116        values.push(Felt::from(selector));
117        values
118    }
119}
120
121impl From<NetworkAccountConfig> for NoteStorage {
122    fn from(config: NetworkAccountConfig) -> Self {
123        NoteStorage::new(config.to_storage_values())
124            .expect("number of storage items should not exceed max storage items")
125    }
126}
127
128// NETWORK ACCOUNT CONFIG NOTE
129// ================================================================================================
130
131/// A NetworkAccountConfig note: adds or removes a root from a network account's note-script
132/// allowlist, tx-script allowlist, or allowed fee policy roots map.
133///
134/// A single note script dispatches on a selector in the note's storage to one of the
135/// [`AuthNetworkAccount`](crate::account::auth::AuthNetworkAccount) component's allowlist or
136/// fee-policy procedures. Authorization is enforced by those procedures through the account-wide
137/// `Authority` component against the note sender.
138///
139/// For the consuming network account to accept this note, its own script root must be in the
140/// account's note script allowlist. Every
141/// [`AuthNetworkAccount`](crate::account::auth::AuthNetworkAccount) allowlists it by default at
142/// construction, so no extra setup is required.
143///
144/// Construct one with the [builder](NetworkAccountConfigNote::builder); convert it into a
145/// protocol [`Note`] infallibly via `Note::from`.
146#[derive(Debug, Clone)]
147pub struct NetworkAccountConfigNote {
148    sender: AccountId,
149    target: AccountId,
150    config: NetworkAccountConfig,
151    serial_number: Word,
152    attachments: NoteAttachments,
153}
154
155#[bon::bon]
156impl NetworkAccountConfigNote {
157    /// Builds a new [`NetworkAccountConfigNote`] that applies `config` to `account`.
158    ///
159    /// The note is bound to `account` with a [`NetworkAccountTarget`] attachment, so that only
160    /// `account` can consume it. This attachment is folded into the note commitment and verified
161    /// on-chain by the note script.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if:
166    /// - `account` does not have [`AccountType::Public`](miden_protocol::account::AccountType).
167    /// - the attachments carry a `NetworkAccountTarget` for an account other than `account`.
168    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]).
169    #[builder]
170    pub fn new(
171        #[builder(field)] mut attachments: Vec<NoteAttachment>,
172        sender: AccountId,
173        target: AccountId,
174        config: NetworkAccountConfig,
175        serial_number: Word,
176    ) -> Result<Self, NoteError> {
177        // Bind consumption to the target account: the note script rejects any other consumer.
178        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
179            NoteError::other_with_source("failed to bind the note to its target account", err)
180        })?;
181
182        let attachments = NoteAttachments::new(attachments)?;
183
184        Ok(Self {
185            sender,
186            target,
187            config,
188            serial_number,
189            attachments,
190        })
191    }
192}
193
194impl NetworkAccountConfigNote {
195    // CONSTANTS
196    // --------------------------------------------------------------------------------------------
197
198    /// Number of storage items of a NetworkAccountConfig note: a selector plus the script
199    /// root word.
200    pub const NUM_STORAGE_ITEMS: usize = 5;
201
202    // PUBLIC ACCESSORS
203    // --------------------------------------------------------------------------------------------
204
205    /// Returns the script of the NetworkAccountConfig note.
206    pub fn script() -> NoteScript {
207        NETWORK_ACCOUNT_CONFIG_SCRIPT.clone()
208    }
209
210    /// Returns the NetworkAccountConfig note script root.
211    pub fn script_root() -> NoteScriptRoot {
212        NETWORK_ACCOUNT_CONFIG_SCRIPT.root()
213    }
214
215    /// Returns the account ID of the note's sender (the account authorized for the action).
216    pub fn sender(&self) -> AccountId {
217        self.sender
218    }
219
220    /// Returns the account ID of the managed network account (the account the note is tagged for).
221    pub fn account(&self) -> AccountId {
222        self.target
223    }
224
225    /// Returns the allowlist-mutation action carried by the note.
226    pub fn config(&self) -> NetworkAccountConfig {
227        self.config
228    }
229
230    /// Returns the note's serial number.
231    pub fn serial_number(&self) -> Word {
232        self.serial_number
233    }
234
235    /// Returns the attachments carried by the note.
236    pub fn attachments(&self) -> &NoteAttachments {
237        &self.attachments
238    }
239}
240
241// BUILDER EXTENSIONS
242// ================================================================================================
243
244impl<S: network_account_config_note_builder::State> NetworkAccountConfigNoteBuilder<S> {
245    /// Adds a single attachment to the note.
246    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
247        self.attachments.push(attachment.into());
248        self
249    }
250
251    /// Adds multiple attachments to the note.
252    pub fn attachments(
253        mut self,
254        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
255    ) -> Self {
256        self.attachments.extend(attachments.into_iter().map(Into::into));
257        self
258    }
259}
260
261impl<S: network_account_config_note_builder::State> NetworkAccountConfigNoteBuilder<S>
262where
263    S::SerialNumber: network_account_config_note_builder::IsUnset,
264{
265    /// Draws a serial number from `rng` and sets it on the builder.
266    pub fn generate_serial_number(
267        self,
268        rng: &mut impl FeltRng,
269    ) -> NetworkAccountConfigNoteBuilder<network_account_config_note_builder::SetSerialNumber<S>>
270    {
271        self.serial_number(rng.draw_word())
272    }
273}
274
275// CONVERSIONS
276// ================================================================================================
277
278impl From<NetworkAccountConfigNote> for Note {
279    fn from(note: NetworkAccountConfigNote) -> Self {
280        // NetworkAccountConfig notes carry no assets and are always public for network
281        // execution; the action and its script root live in the note storage.
282        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
283            .with_tag(NoteTag::with_account_target(note.target));
284        let recipient = NoteRecipient::new(
285            note.serial_number,
286            NetworkAccountConfigNote::script(),
287            NoteStorage::from(note.config),
288        );
289
290        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
291    }
292}
293
294// NOTE CONSUMPTION COST
295// ================================================================================================
296
297impl NoteConsumptionCost for NetworkAccountConfigNote {
298    fn consumption_cycles() -> u32 {
299        NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES
300    }
301}
302
303// TESTS
304// ================================================================================================
305
306#[cfg(test)]
307mod tests {
308    use alloc::vec::Vec;
309
310    use miden_protocol::account::AccountType;
311    use miden_protocol::crypto::rand::RandomCoin;
312
313    use super::*;
314
315    fn account_id(seed: u8) -> AccountId {
316        AccountId::builder()
317            .account_type(AccountType::Public)
318            .build_with_seed([seed; 32])
319    }
320
321    fn note_root(seed: u32) -> NoteScriptRoot {
322        NoteScriptRoot::from_array([seed, seed + 1, seed + 2, seed + 3])
323    }
324
325    fn tx_root(seed: u32) -> TransactionScriptRoot {
326        TransactionScriptRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
327    }
328
329    fn policy_root(seed: u32) -> AccountProcedureRoot {
330        AccountProcedureRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
331    }
332
333    /// The builder produces a public, asset-less note tagged for the managed network account.
334    #[test]
335    fn builder_builds_allowlist_config_note() {
336        let mut rng = RandomCoin::new(Word::empty());
337        let account = account_id(1);
338        let sender = account_id(2);
339
340        let note = NetworkAccountConfigNote::builder()
341            .sender(sender)
342            .target(account)
343            .config(NetworkAccountConfig::AddAllowedNoteScript { script_root: note_root(10) })
344            .generate_serial_number(&mut rng)
345            .build()
346            .unwrap();
347
348        assert_eq!(note.sender(), sender);
349        assert_eq!(note.account(), account);
350
351        let note = Note::from(note);
352        assert_eq!(note.metadata().note_type(), NoteType::Public);
353        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(account));
354        assert_eq!(note.assets().num_assets(), 0);
355
356        // The note is bound to its target account by a NetworkAccountTarget attachment
357        let target = NetworkAccountTarget::try_from(note.attachments())
358            .expect("note must carry a network account target attachment");
359        assert_eq!(target.target_id(), account);
360    }
361
362    /// Storage is `[ROOT, selector]` with the selector matching the action kind.
363    #[test]
364    fn storage_layout() {
365        let note_root = note_root(10);
366        let tx_root = tx_root(20);
367        let policy_root = policy_root(30);
368
369        let cases = [
370            (
371                NetworkAccountConfig::AddAllowedNoteScript { script_root: note_root },
372                NetworkAccountConfig::SELECTOR_ADD_ALLOWED_NOTE_SCRIPT,
373                note_root.as_word(),
374            ),
375            (
376                NetworkAccountConfig::RemoveAllowedNoteScript { script_root: note_root },
377                NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT,
378                note_root.as_word(),
379            ),
380            (
381                NetworkAccountConfig::AddAllowedTxScript { script_root: tx_root },
382                NetworkAccountConfig::SELECTOR_ADD_ALLOWED_TX_SCRIPT,
383                tx_root.as_word(),
384            ),
385            (
386                NetworkAccountConfig::RemoveAllowedTxScript { script_root: tx_root },
387                NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_TX_SCRIPT,
388                tx_root.as_word(),
389            ),
390            (
391                NetworkAccountConfig::AddAllowedFeePolicy { policy_root },
392                NetworkAccountConfig::SELECTOR_ADD_ALLOWED_FEE_POLICY,
393                policy_root.as_word(),
394            ),
395            (
396                NetworkAccountConfig::RemoveAllowedFeePolicy { policy_root },
397                NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_FEE_POLICY,
398                policy_root.as_word(),
399            ),
400        ];
401
402        for (action, selector, root_word) in cases {
403            let storage = NoteStorage::from(action);
404            let mut expected = Vec::from(root_word.as_elements());
405            expected.push(Felt::from(selector));
406            assert_eq!(storage.items(), expected.as_slice());
407            assert_eq!(storage.items().len(), NetworkAccountConfigNote::NUM_STORAGE_ITEMS);
408        }
409    }
410
411    /// The action-note script root is registered in the [`StandardNote`](crate::note::StandardNote)
412    /// reverse lookup.
413    #[test]
414    fn script_root_is_registered_standard_note() {
415        use crate::note::StandardNote;
416
417        let standard = StandardNote::from_script_root(NetworkAccountConfigNote::script_root())
418            .expect("config note script root should be a registered standard note");
419        assert_eq!(standard.name(), "NETWORK_ACCOUNT_CONFIG");
420    }
421}