Skip to main content

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